SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

Overview

Articles related to this project


Clean Architecture for SwiftUI + Combine

A demo project showcasing the setup of the SwiftUI app with Clean Architecture.

The app uses the restcountries.eu REST API to show the list of countries and details about them.

Check out mvvm branch for the MVVM revision of the same app.

For the example of handling the authentication state in the app, you can refer to my other tiny project that harnesses the locks and keys principle for solving this problem.

platforms Build Status codecov codebeat badge

Diagram

Key features

  • Vanilla SwiftUI + Combine implementation
  • Decoupled Presentation, Business Logic, and Data Access layers
  • Full test coverage, including the UI (thanks to the ViewInspector)
  • Redux-like centralized AppState as the single source of truth
  • Data persistence with CoreData
  • Native SwiftUI dependency injection
  • Programmatic navigation. Push notifications with deep link
  • Simple yet flexible networking layer built on Generics
  • Handling of the system events (such as didBecomeActive, willResignActive)
  • Built with SOLID, DRY, KISS, YAGNI in mind
  • Designed for scalability. It can be used as a reference for building large production apps

Architecture overview

Diagram

Presentation Layer

SwiftUI views that contain no business logic and are a function of the state.

Side effects are triggered by the user's actions (such as a tap on a button) or view lifecycle event onAppear and are forwarded to the Interactors.

State and business logic layer (AppState + Interactors) are navitely injected into the view hierarchy with @Environment.

Business Logic Layer

Business Logic Layer is represented by Interactors.

Interactors receive requests to perform work, such as obtaining data from an external source or making computations, but they never return data back directly.

Instead, they forward the result to the AppState or to a Binding. The latter is used when the result of work (the data) is used locally by one View and does not belong to the AppState.

Previously, this app did not use CoreData for persistence, and all loaded data were stored in the AppState.

With the persistence layer in place we have a choice - either to load the DB content onto the AppState, or serve the data from Interactors on an on-demand basis through Binding.

The first option suits best when you don't have a lot of data, for example, when you just store the last used login email in the UserDefaults. Then, the corresponding string value can just be loaded onto the AppState at launch and updated by the Interactor when the user changes the input.

The second option is better when you have massive amounts of data and introduce a fully-fledged database for storing it locally.

Data Access Layer

Data Access Layer is represented by Repositories.

Repositories provide asynchronous API (Publisher from Combine) for making CRUD operations on the backend or a local database. They don't contain business logic, neither do they mutate the AppState. Repositories are accessible and used only by the Interactors.


Twitter blog venmo

Comments
  • Question about reacting to state change

    Question about reacting to state change

    Hi!

    First of all, a very awesome project and article on clean architecture with SwiftUI.

    I have a question though.

    You say Instead, they forward the result to the AppState or to a Binding. The latter is used when the result of work (the data) is used locally by one View and does not belong to the AppState.

    For the first case, I can subscribe to any data change in Store by using something like routingUpdate and then .onReceive(routingUpdate) { _ in }. But I am a bit struggling with the latter case - I pass a binding to an Interactor when the data is only applicable to one view. I modify the binding in that Interactor, my views react to the change, but I also want to subscribe that change in the view and do some additional actions/interactions. How would I achieve that?

    opened by NeverwinterMoon 16
  • [Question] How would you react to state changes in view

    [Question] How would you react to state changes in view

    Hi there !

    At first thank you for providing this repository and your related blog posts. I enjoyed reading them and they helped me a lot.

    But I have a question about reacting to @State changes in Views in this architecture.


    For example I have the following case in my application:

    I have a view which displays a list of tasks for a specific date interval. It has your dependency injection container injected:

    @Environment(\.injected) private var injected: DIContainer
    @State private var dateInterval: DateInterval = DateInterval(start: Date().startOfWeek ?? Date(), end: Date().endOfWeek ?? Date())
    

    Initially the data is fetched with a method that looks like this:

    func fetch() {
        injected.interactors.myInteractor
            .fetchData(range: self.dateInterval)
            .store(in: cancelBag)
    }
    

    The view has a child which accepts a binding to the dateInterval and is able to change it:

     WeeksTaskGraph(dateInterval: self.$dateInterval, tasks: tasks)
    

    Now I need to refetch data when the binding changes. So basically I would need to run the fetch method again when the dateInterval changes. I tried to create an ObservableObject ViewModel class for the view, for encapsulating this kind of functionality. It looks roughly like this:

    class DateIntervalModel: ObservableObject {
        
        private let cancelBag = CancelBag()
        
        @Environment(\.injected) private var injected: DIContainer
        
        @Published var dateInterval: DateInterval = DateInterval(start: Date().startOfWeek ?? Date(), end: Date().endOfWeek ?? Date()) {
            didSet {
                print("Date interval changed")
                 injected.interactors.myInteractor.fetchData(range: self.dateInterval)
                    .store(in: cancelBag)
            }
        }
    }
    

    But as seen this class would then need access to the @EnvironmentObject which I could not find a solution how to achieve that - since it is not part of the view hierarchy.

    Do you maybe have an approach or a suggestion how this can be achieved with the clean architecture ?

    Any help is appreciated ! If this is the wrong place for asking this kind of questions feel free to close the issue or tell me so.

    opened by alexanderwe 13
  • Examples of custom full-screen transition animations based on Routing

    Examples of custom full-screen transition animations based on Routing

    Would you consider adding an example of full-screen view transition animations based on Routing to this project? Something where both - the new view and the previous view - would have different transition animations. Say, previous view scales down and the new view slides in.

    opened by NeverwinterMoon 11
  • More Please!

    More Please!

    This is by far the best SwiftUI example I have found to date (and I keep up on this stuff!)

    I was wondering if you have any plans of adding some user actions, I am particularly curious how you would handling deletion of an item from a list. Again thank you so much for this resource.

    opened by garsdle 7
  • Access contents of lazylist

    Access contents of lazylist

    Awesome template for clean architecture!

    I've the below combine pipeline which is the opposite of way you're handling data in this template; In most real world scenarios;

    • data is first fetched from the web
    • then stored in the db
    • if fetching from web fails
    • load from db
    func refreshCards(id: String) -> AnyPublisher<Void, Error> {
        var cards: [Card] = []
        return gateway
        .listCards(customerID: id)
        .map {
            response in
            response.cards.forEach {
                cards.append(Card(protoObject: $0)!)
            }
            return cards
        }
    
        .catch {
            err in
            // must return a Publisher
            repository.cards()
        }
    
        .flatMap {
            [repository] in
            repository.store(cards: $0) // Cannot convert value of type 'LazyList<Card>' to expected argument type '[Card]'
        }
    
        .eraseToAnyPublisher()
    }
    

    repository.store(cards: ) accepts an array of cards - [Card] - how can I unpack LazyList and turn it into an array?

    opened by moh-abk 5
  • @Enviroment vs @EnviromentObject

    @Enviroment vs @EnviromentObject

    I am pretty new to iOS development and I don't have background of Javascript and UIKit. I am learning SwiftUI directly.

    Why did you used @Enviroment and not @EnviromentObject in your project? Can you please add comments in your code so it will more useful for beginners like me.

    Thanks for creating this project and post related to it. It was very helpful and appreciated :)

    opened by ramtest55 5
  • App doesn't build

    App doesn't build

    /UnitTests/Mocks/MockedInteractors.swift:38:14: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'loadCountryDetails(_:)' (identifier_name)

    /UnitTests/Mocks/MockedInteractors.swift:63:14: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'loadImage(_:)' (identifier_name)

    /UnitTests/Mocks/MockedWebRepositories.swift:25:14: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'loadCountryDetails(_:)' (identifier_name)

    /UnitTests/Mocks/MockedWebRepositories.swift:48:14: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'loadImage(_:)' (identifier_name)

    /CountriesSwiftUI/Repositories/CountriesWebRepository.swift:49:14: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'countryDetails(_:)' (identifier_name)

    /CountriesSwiftUI/Utilities/APICall.swift:20:10: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'httpCode(_:)' (identifier_name)

    /CountriesSwiftUI/System/SystemEventsHandler.swift:80:14: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'showCountryFlag(alpha3Code:)' (identifier_name)

    /CountriesSwiftUI/Utilities/Loadable.swift:14:10: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'isLoading(last:)' (identifier_name)

    /CountriesSwiftUI/Utilities/Loadable.swift:15:10: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'loaded(_:)' (identifier_name)

    /CountriesSwiftUI/Utilities/Loadable.swift:16:10: Identifier Name Violation: Enum element name should only contain alphanumeric characters: 'failed(_:)' (identifier_name)

    opened by GJNilsen 5
  • Cannot connect to REST API

    Cannot connect to REST API

    Hey,

    fist off thanks for this insightful repository and article!

    When cloning the repository and trying to run it, the application can not connect to the REST API restcountries.eu. Trying to access it via a browser redirects to countrylayer.com. I'm not sure what happened with the API or if it is something that just temporarily happened to me.

    Kind regards

    opened by stef-t 4
  • cancel bag is not needed

    cancel bag is not needed

    AnyCancellable is doing cancel on deinit, it called RAII pattern: https://developer.apple.com/documentation/combine/anycancellable

    An AnyCancellable instance automatically calls cancel() when deinitialized
    

    No need to call explicit cancel() on items. CancelBag looks like its not needed. It will cleanup a lot of code

    opened by sisoje 4
  • Question: presenting another same view in view hierarchy

    Question: presenting another same view in view hierarchy

    Hello Alex,

    Thank you for your architecture example and articles, they are awesome and help me a lot to understand how to work with SwiftUI.

    I would like to ask you about more complex navigation in the example. For example, the application has to show CountryDetails from ModalDetailsView. I tried to implement this in fork https://github.com/iomark/clean-architecture-swiftui/tree/test/same_view . As you can see it leads to weird effects. Could you advise how to resolve similar use cases related to using a view that already is presented in view hierarchy? As I understand the application routing state should resolve it in some way.

    opened by iomark 4
  • Consider turning this project into a tutorial

    Consider turning this project into a tutorial

    Hey there,

    I am a novice in programming, and I struggle grasping architectures very much. That SwiftUI / Combine are so new does not help a bit, as the dust is definitely not settled yet, and seems like very few really understand how to design with new frameworks.

    This project (just as your posts btw) reads very easily, yet I still struggle understanding the core 'why's of the architecture, not to say that I cannot see the whole picture well.

    If turned into a tutorial with detailed explanations and rationales describing the architecture, this becomes a goto place to learn how SwiftUI / Combine apps are designed. By tutorial I mean something akin to Apple's (unfortunately very non-informative) multi-step tutorial residing on /tutorials page.

    This seems like not an easy task, but maybe other experienced devs can join this initiative.

    Thanks!

    opened by shengchalover 4
  • How do I add pagination?

    How do I add pagination?

    I am newbie in swiftui.

    I am trying to add Pagenation in this feature. If I try to use append as values in loadable<[Model]> , I will get get-only property error.

    Model.value.append($0)

    How can i fix it?

    opened by vasicer 0
  • app crash after select some countires

    app crash after select some countires

    Screen Shot 2022-06-02 at 17 28 48 2022-06-02 17:14:07.367568+0800 CountriesSwiftUI[18270:247217] [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7f95678e8000; baseClass = UITableView; frame = (0 0; 414 896); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = ; layer = ; contentOffset: {0, -140}; contentSize: {414, 847.5}; adjustedContentInset: {140, 0, 34, 0}; dataSource: <_TtGC7SwiftUIP13$7fff2c69da4419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x7f9565e4d180>> (lldb)

    Simulator Screen Shot - Xr 13 3 - 2022-06-02 at 17 28 55 Uploading Screen Shot 2022-06-02 at 17.28.48.png…

    opened by stoneyanjun 1
  • Reseting route state

    Reseting route state

    Hit @nalexn, great job on you repo. I have a question. How and when do you reset your routing states? Currently I have a hierarchy like below: SplashView -> AuthenticationView -> SignUpView -> HomeView I want to log out on home view and go back (move forward) to authentication view. when I change the routing state on home view and go to authentication view, all the previous flags for navigation are true and therefore the view goes forward again and again.

    opened by MohsenKhosravinia 5
  • Find by AccessibilityIdentifier

    Find by AccessibilityIdentifier

    There's a find(viewWithId:) function, but as I already set the accessibilityIdentifier property on all views for UI testing, I needed a new find function. I implemented this as a shortcut:

    extension InspectableView { func find(_ id: String) throws -> InspectableView<ViewType.ClassifiedView> { try find { try $0.accessibilityIdentifier() == id } } }

    Would be nice to see something like this integrated into the standard package as probably everyone doing UI testing already has those identifiers set.

    opened by gmcadosch 3
Owner
Alexey Naumov
Designing software the ruthless way
Alexey Naumov
MoneySafe - Application for tracking income and expenses and analyzing expenses. VIPER architecture, CoreData, Charts

?? MoneySafe ?? Application for tracking income and expenses and analyzing expen

Vladislav 5 Dec 27, 2022
Example of Clean Architecture of iOS app using RxSwift

Clean architecture with RxSwift Contributions are welcome and highly appreciated!! You can do this by: opening an issue to discuss the current solutio

null 3.6k Dec 29, 2022
This is an example of clean architecture and MVVM pattern written in swift

Swift Clean Architecture MVVM This is an example of clean architecture and MVVM pattern written in swift First of all thanks to all of those who made

null 19 Oct 12, 2022
Clean Actor Model Architecture

CAMA Eonil, 2021. CAMA is short for "Clean Actor Model Architecture". As like it says, its CA(Clean Architecture) + AM(Actor model). AM here means AM

Eonil 1 Oct 10, 2021
Sample project using VIPER architecture

VIPER-Persons Small project using a master and detail view Demonstrates the use of the following features: VIPER architecture (https://mutualmobile.co

Sebastian Wramba 43 Feb 11, 2022
Sample Code of the App Architecture Book

App Architecture iOS Application Design Patterns in Swift Welcome to the repository of the App Architecture book! This repository hosts all sample cod

objc.io 2k Dec 29, 2022
Spin aims to provide a versatile Feedback Loop implementation working with the three main reactive frameworks available in the Swift community (RxSwift, ReactiveSwift and Combine)

With the introduction of Combine and SwiftUI, we will face some transition periods in our code base. Our applications will use both Combine and a thir

Spinners 119 Dec 29, 2022
Sample applications of iOS Design patterns written using swift.

ios-design-patterns This repo contains all my Sample applications of iOS Design patterns written using swift. Link for my Design patterns Blog : https

ShreeThaanu Raveendran 3 Nov 2, 2022
Reactant is a reactive architecture for iOS

Reactant Reactant is a foundation for rapid and safe iOS development. It allows you to cut down your development costs by improving reusability, testa

Brightify 374 Nov 22, 2022
Swift Interaction with VIPER Architecture

SwiftyVIPER SwiftyVIPER allows easy use of VIPER architecture throughout your iOS application. VIPER Architecture What is VIPER? Great question! VIPER

Cody Winton 121 Jan 2, 2023
YARCH iOS Architecture

YARCH is an architecture pattern developed primarly for iOS applications. You can ask any questions in our telegram channel. Russian version of the re

Alfa Digital 196 Jan 3, 2023
Challenge-viper-finance - Project for VIPER Architecture Dev Sprints on Devpass

VIPER Challenge - Finance App ?? Neste desafio, aplicaremos conceitos da arquite

Devpass 15 Oct 11, 2022
Viper Framework for iOS using Swift

Write an iOS app following VIPER architecture. But in an easy way. Viper the easy way We all know Viper is cool. But we also know that it's hard to se

Ferran Abelló 504 Dec 31, 2022
Stateful view controller containment for iOS and tvOS

StateViewController When creating rich view controllers, a single view controller class is often tasked with managing the appearance of many other vie

David Ask 309 Nov 29, 2022
A holistic approach to iOS development, inspired by Redux and MVVM

Tempura is a holistic approach to iOS development, it borrows concepts from Redux (through Katana) and MVVM. ?? Installation Requirements CocoaPods Sw

Bending Spoons 693 Jan 4, 2023
A Swift 4.2 VIPER Module Boilerplate Generator with predefined functions and a BaseViewProtocol.

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. Are you new to VIPER Design Pattern? Want

Mohamad Kaakati 68 Sep 29, 2022
A collection of iOS architectures - MVC, MVVM, MVVM+RxSwift, VIPER, RIBs and many others

ios-architecture WIP ?? ?? ?? ??️ Demystifying MVC, MVVM, VIPER, RIBs and many others A collection of simple one screen apps to showcase and discuss d

Pawel Krawiec 1.3k Jan 3, 2023
SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

Articles related to this project Clean Architecture for SwiftUI Programmatic navigation in SwiftUI project Separation of Concerns in Software Design C

Alexey Naumov 4k Dec 31, 2022
SugarRecord is a persistence wrapper designed to make working with persistence solutions like CoreData in a much easier way.

What is SugarRecord? SugarRecord is a persistence wrapper designed to make working with persistence solutions like CoreData in a much easier way. Than

Modo 2.1k Dec 29, 2022
 Sample iOS App - A collection of examples and patterns for Unit Testing, UI Testing, handling Result/Optionals, writing documentation

 Sample iOS App - A collection of examples and patterns for Unit Testing, UI Testing, handling Result/Optionals, writing documentation, and more

Soojin Ro 206 Dec 9, 2022