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
  • Question regarding Value: Decodable and Closures

    Question regarding Value: Decodable and Closures

    Hey there, I'm new to iOS development in general but especially Combine/SwiftUI. I've followed your clean architecture and tried to extend it a bit by introducing credentials manager and injecting a token in the WebRepository call.

    However, what puzzles me is that if I wrap following code below into a closure I get Generic parameter 'Value' could not be inferred.

    session
      .dataTaskPublisher(for: request)
      .requestJSON(httpCodes: httpCodes)
    

    Could you please advise on how to get this solved? This is the whole picture of the function.

    func call<Value>(endpoint: APICall, httpCodes: HTTPCodes = .success) -> AnyPublisher<Value, Error>
        where Value: Decodable
    {
        do {
            var request = try endpoint.urlRequest(baseURL: baseURL)
    
            credentialsManager.credentials()
                .map { (credentials: Credentials) in
                    let token = credentials.accessToken
                    request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
                }
                .map { (_ publisher: AnyPublisher<Value, Error>) in
                    session
                        .dataTaskPublisher(for: request) // Generic parameter 'Value' could not be inferred
                        .requestJSON(httpCodes: httpCodes)
                }
    
        } catch {
            return Fail<Value, Error>(error: error).eraseToAnyPublisher()
        }
    }
    
    opened by fredagsfys 0
  • 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
Owner
Alexey Naumov
Designing software the ruthless way
Alexey Naumov
News App 📱 built to demonstrate the use of SwiftUI 3 features, Async/Await, CoreData and MVVM architecture pattern.

Box Feed News App ?? built to demonstrate the use of following features, SwiftUI 3 Async/Await AsyncImage List Refreshable Swipe Actions Separator Cor

Sameer Nawaz 113 Dec 20, 2022
Github repo search with using mvvm-c and clean architecture and using combine swift

GitSearchWithMVVM-C-CleanArchitecture Github repo search with using mvvm-c and clean architecture and using combine swift. Content Overview How To Run

Muhammad Qasim Majeed 1 Mar 16, 2022
Mahmoud-Abdelwahab 5 Nov 23, 2022
RippleQueries is an iOS application built as assessment task at Ripple Egypt. Built Using MVVM (Model-View-ViewModel) and Clean Architecture concepts

RippleRepositories RippleRepositories is an iOS application built as an assessment task at Ripple Egypt. Built Using RxSwift & MVVM (Model-View-ViewMo

Muhammad Ewaily 3 Sep 16, 2021
A Flutter Clean Architecture Using GetX.

flutter-getx-clean-architecture A Flutter Clean Architecture Using GetX. Work Flow Project Structure |-- lib |-- main.dart |-- app |--

Duc Pham 93 Dec 27, 2022
Hello Clean Architecture With SwiftUI

HelloCleanArchitectureWithSwiftUI CleanArchitecture for SwiftUI with Combine, Co

null 8 Nov 14, 2022
Weather Forecast App (OpenWeather API & CLLocationManager). Clean Swift VIP architecture.

WeatherApp Weather Forecast App (OpenWeather API & CLLocationManager). Clean Swift VIP architecture. Without storyboard or xib. The application shows

Nikita Lomovtsev 7 Dec 25, 2022
App for displaying VK news feed (VKSDK API). Clean Swift VIP architecture

VKNewsFeed VKNewsFeed - application for displaying VK news feed with dynamic cells and a collection of images in the post. Data request occurs from th

Areg Vardanian 0 Dec 18, 2021
Assignment: iOS app in VIP Clean architecture

countries_vip_clean Assignment: iOS app in VIP Clean architecture. for countries

Vishwa Deepak Choudhary 1 Feb 7, 2022
A Simple ToDo app developed using SwiftUI, Combine and Coredata

SwiftUI_Tasks Tasks is simple ToDo app developed using SwiftUI and Coredata which having features like Add,Delete,Rearrange and send notification base

Shankar Madeshvaran 77 Dec 18, 2022
Best architecture for SwiftUI + CombineBest architecture for SwiftUI + Combine

Best architecture for SwiftUI + Combine The content of the presentation: First of the proposed architectures - MVP + C Second of the proposed architec

Kyrylo Triskalo 3 Sep 1, 2022
MVP-Clean sample iOS Swift project

RestaurantsApp MVP-Clean sample iOS Swift project The purpose of this document is to explain the architecture of application. This application shows r

Atul Ghorpade 2 May 1, 2022
This is a mastodon sample SwiftUI app implemented with the architecture of state management with normalized cache.

MastodonNormalizedCacheSample This is a mastodon sample SwiftUI app. This app is implemented with the architecture of state management with Normalized

null 5 Nov 27, 2022
Examples projects using SwiftUI released by WWDC2019. Include Layout, UI, Animations, Gestures, Draw and Data.

SwiftUI Examples About Examples projects using SwiftUI & Combine. Include Layout, UI, Animations, Gestures, Draw and Data. See projects files in Files

Ivan Vorobei 4.2k Jan 6, 2023
An iOS application written in Swift to demonstrate how to implement a Clean Architecture in iOS

Reminders iOS An iOS application written in Swift to demonstrate how to implement a Clean Architecture in iOS. Idea The idea is to implement the simpl

Tiago Martinho 306 Nov 9, 2022
An example of using SwiftUI + CoreData

ShoppingList An example of using SwiftUI + CoreData Screenshots ||| Running Must use the latest Xcode 11 beta, and be running some sort of beta softwa

Eric Lewis 12 Feb 11, 2022
Mvi Architecture for SwiftUI Apps. MVI is a unidirectional data flow architecture.

Mvi-SwiftUI If you like to read this on Medium , you can find it here MVI Architecture for SwiftUI Apps MVI Architecture Model-View-Intent (MVI) is a

null 12 Dec 7, 2022
Swiftui-pressed-states-example - Examples of Pressed States in SwiftUI

Examples of Pressed States in SwiftUI pressed-states.mp4

Philip Davis 6 Nov 15, 2022
Simple SwiftUI + CoreData app

Footnote Hello! Before you interact with this repository, please check out our Code of Conduct and Contributing Guidelines. Footnote is a SwiftUI + Co

Cameron Bardell 37 Oct 17, 2022