Giter VIP home page Giter VIP logo

ioscleanarchitecture's Introduction

iOSCleanArchitecture

iOS Clean Architecture with UIKit, MVVM, RxSwift

High level overview

alt text

The whole design architecture is separated into 4 rings:

  • Entities: Enterprise business rules
  • UseCases: Application business rules
  • Data: Network & Data persistent
  • Application: UI & Devices

The most important rule is that the inner ring knows nothing about outer ring. Which means the variables, functions and classes (any entities) that exist in the outer layers can not be mentioned in the more inward levels.

Detail overview

alt text

Domain

Entities are implemented as Swift struct

struct Article: Decodable {
    @Default.Empty var author: String
    @Default.Empty var title: String
    @Default.Empty var description: String
    @Default.Empty var url: String
    @Default.Empty var urlToImage: String
    @Default.Empty var publishedAt: String
    @Default.Empty var content: String
}

UseCases are protocols

protocol ArticleUseCase {
    func findArticlesByKeyword(_ keyword: String, pageSize: Int, page: Int) -> Single<[Article]>
}

Domain layer doesn't depend on UIKit or any 3rd party framework.

Data

Repositories are concrete implementation of UseCases

struct SearchArticleResult: Decodable {
    @Default.EmptyList var articles: [Article]
    @Default.Zero var totalResults: Int
}

struct ArticleRepository: ArticleUseCase {
    func findArticlesByKeyword(_ keyword: String, pageSize: Int, page: Int) -> Single<[Article]> {
        return ArticleService
            .searchArticlesByKeyword(q: keyword, pageSize: pageSize, page: page)
            .request(returnType: SearchArticleResult.self)
            .map { $0.articles }
    }
}

Application

Application is implemented with the MVVM pattern. The ViewModel performs pure transformation of a user Input to the Output

protocol ViewModelProtocol {
    associatedtype Input
    associatedtype Output

    func transform(input: Input) -> Output
}
struct ArticleListViewModel: ViewModelProtocol {
    struct Input {
        let search: Observable<String>
        let loadMore: Observable<Void>
    }

    struct Output {
        let tableData: Driver<[SectionModel]>
        let fetching: Driver<Bool>
        let error: Driver<Error>
    }

    @Injected var articleUseCase: ArticleUseCase

    func transform(input: Input) -> Output {
        .....
        Observable.merge(search, loadMore)
            .flatMapLatest { keyword in
                return articleUseCase
                    .findArticlesByKeyword(keyword, pageSize: pageSize, page: currentPage.value)
                    .trackActivity(activityTracker)
                    .trackError(errorTracker)
                    .asDriver(onErrorJustReturn: [])
            }
            .subscribe(onNext: { articles in
        .....
    }
}

As you can see, articleUseCase is injected to ViewModel by @Injected annotation. Thanks to Resolver library to make dependency injection easier.

The ViewModel is injected to ViewController via Navigator

struct ArticleNavigator {
    let navigationController: UINavigationController

    func showArticles() {
        let articleListViewController = Storyboard.load(.article, type: ArticleListViewController.self)
        articleListViewController.viewModel = ArticleListViewModel()
        articleListViewController.navigator = self
        navigationController.pushViewController(articleListViewController, animated: false)
    }
    .....
final class ArticleListViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!

    private let bag = DisposeBag()

    var viewModel: ArticleListViewModel!
    var navigator: ArticleNavigator!
    .....

Testing

What to test

In this architecture ViewModels, UseCases and Entities (if they contains business logic) can be tested.

ViewModel tests

To test the ViewModel you should have the RepositoryMock

struct ArticleRepositoryMock: ArticleUseCase {
    func findArticlesByKeyword(_ keyword: String, pageSize: Int, page: Int) -> Single<[Article]> {
        return MockLoader
            .load(returnType: SearchArticleResult.self, file: "searchArticles.json")
            .map { $0.articles }
    }
}
typealias ViewModel = ArticleListViewModel

class ArticleListViewModelTests: XCTestCase {

    var testScheduler: TestScheduler!
    var viewModel: ViewModel!
    let bag = DisposeBag()

    override func setUpWithError() throws {
        testScheduler = TestScheduler(initialClock: 0)
        viewModel = ArticleListViewModel()
        viewModel.articleUseCase = ArticleRepositoryMock()
    }

    override func tearDownWithError() throws {}

    func test_searchWithKeyword() throws {
        // Given
        let search = testScheduler
            .createHotObservable([
                .next(0, "Tesla")
            ])
            .asObservable()
        let input = ViewModel.Input(search: search, loadMore: .never())
        let output = viewModel.transform(input: input)

        // When
        testScheduler.start()
        let articles = try! output.tableData.articles.toBlocking().first()!

        // Then
        XCTAssertEqual(articles.count, 20)
        XCTAssertEqual(articles[1].author, "Mike Murphy")
    }

    func test_loadMore() throws {
        // Given
        let search = testScheduler
            .createHotObservable([
                .next(0, "Tesla")
            ])
            .asObservable()
        let loadMore = testScheduler
            .createHotObservable([
                .next(2, ())
            ])
            .asObservable()
        let input = ViewModel.Input(search: search, loadMore: loadMore)
        let output = viewModel.transform(input: input)

        // When
        testScheduler.start()
        let articles = try! output.tableData.articles.toBlocking().first()!
        
        // Then
        XCTAssertEqual(articles.count, 40)
    }

Code generator

The clean architecture, MVVM or VIPER will create a lot of files when you start a new module. So using a code generator is the smart way to save time.

codegen is a great tool to do it.

ioscleanarchitecture's People

Contributors

dinhquan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.