Giter Club home page Giter Club logo

clean-architecture-swiftui's Introduction

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.com 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 natively 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

clean-architecture-swiftui's People

Contributors

araiyusuke avatar greyvend avatar lgvv avatar nalexn avatar schinj avatar torburg avatar

Stargazers

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

Watchers

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

clean-architecture-swiftui's Issues

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)

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.

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?

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.

Modular Architecture Support

I like the architecture. But I'm wondering how we can share the App State through other modules and how other modules can change that state. If you have some thoughts on this please advise.

@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 :)

License

Such a great example, thank you! Could you add a license to the project?

Question: Handling multiple loadables

How would you deal with views that require more than one loadable?

Your view private var content: AnyView always switches on the status of one loadable, but I think I have a situation where I need to wait for multiple loaders to have loaded before drawing a loaded view.

I figure I could use one loader and pass it a tuple, but that seems really messy.
I wonder if I want some kind of loader container could be the way to go?

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.

Example of handling notifications using the clean architecture

Would be nice to see an example of how to handle notifications, especially the silent ones that would trigger some data fetch/update.

Seeing as notifications are handled in AppDelegate callback, how would one go around hooking the logic from, say, Interactors.

So far, I see that it's possible to handle non-silent notifications in scene delegate like this:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      let environment = AppEnvironment.bootstrap(window: window)


      // This is UNNotificationResponse
      if let notificationResponse = connectionOptions.notificationResponse {
        window.makeKeyAndVisible()

        // do something with environment.container.interactors

        return
      }

      window.makeKeyAndVisible()
    }
  }
}

And for silent notifications:

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable: Any],
  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> ()
) {
  if let sceneDelegate = application.connectedScenes.first?.delegate as? SceneDelegate {
    sceneDelegate.handleSilentNotification(userInfo)
    completionHandler(.newData)
  }

  completionHandler(.noData)
}

Now that Xcode 11.4 allows to test notifications even on a simulator, it would be great to see the notification handling introduced to the example Clean SwiftUI Architecture app.

What is the propose of `inspect` on RootViewApparence?

When and why inspection = PassthroughSubject<((AnyView) -> Void), Never>() is used on RootViewApparence ?

.onReceive(inspection) { callback in
callback(AnyView(self.body(content: content)))
}

I was checking ce82dfe however I am not sure if that was added only due to tests.

Another thing that kept me wondering what is the utility of that property is that you have used ViewInspector on other parts of the project. Wouldn't be the case of using ViewInspector for testing the RootViewApparence as well ?

@ObservedObject ViewModels

Hi Alexey.

I see you are using @ObservedObject for ViewModels in MVVM branch. Earlier you posted an article why you quit using @ObservableObject. The main point of not using it is that you cannot simply compare ViewModels when implementing Equatable for the views because they reference the same ViewModels.

Do you have any idea on how implement Equatable for the views if the state is stored inside ViewModels?

How about making the example do modifications?

This is an interesting model (I'm looking at the MVVM version)

Would you be able to implement updates via the UI into CoreData? It looks like the sample implementation implements the read path. I'd be curious how the write path works, specifically:

  1. I'm curious how you would implement TextField updates (for example, to let the user change the name of a country). Simple implementations would bind the TextField to the View Model and then (maybe?) use a combine pipeline to write updates to the view model to the DB. How would you convert this to an action?
  2. I'm also curious how you would handle CoreData synchronizations from iCloud. How does this model detect changes coming from the user (user edits the country) and thus the change needs to flow to CoreData, vs a change that happens in CoreData (maybe they have the app loaded on another device and changed the country there) needing to update the UI. How would you handle conflicts in this model?

Thank you for the cool work!

Question: Accessing Loadable state outside CountriesList

Hi,

Thanks for the awesome repository! It is a great source for beginners in IOS development to learn the basics of Architecture in SwiftUI Programming!

I was wondering based on your current solution, how would you expose the current state of the loadable to the parent component? For example, you want to show something else in the parent as soon as the loadable in the child is finished loading.

Thanks!

Question: about your article of stranger-things-swiftui-state

Hi
First of all, thank you
I'm learning a lot about clean architecture for Swift UI from you

stranger-things-swiftui-state in the "Clean Architecture for SwiftUI" article, "Coordinator is history" Section
In this scenario,

struct TestView: View {

    let publisher = PassthroughSubject<String, Never>()    // 1
    let publisher = CurrentValueSubject<String, Never>("") // 2
    
    var body: some View {
        MyView(publisher: publisher.eraseToAnyPublisher())
    }
}

Scenario 2 works differently.

Here's my code.

import SwiftUI

@main
struct TestSwiftUIApp: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
import SwiftUI
import Combine

struct ContentView: View {
    let publisher = CurrentValueSubject<String, Never>("") // 2
    var body: some View {
        MyView(publisher: publisher.eraseToAnyPublisher())
    }
    
}

struct MyView: View {
    
    let publisher: AnyPublisher<String, Never>
    
    @State var text: String = ""
    @State var didAppear: Bool = false
    
    var body: some View {
        Text(text)
            .onAppear { self.didAppear = true }
            .onReceive(publisher) {
                print("onReceive")
                self.text = $0
            }
    }
}

image

my answer is 1...

also

struct MyView: View {
    
    let publisher: AnyPublisher<String, Never>
    
    @State var text: String = ""
    @State var didAppear: Bool = false
    
    var body: some View {
        Text(text)
            .onAppear { self.didAppear = true
                self.text = "abc"
            }
            .onReceive(publisher) {
                print("onReceive")
                self.text = $0
            }
    }
}

my answer is "abc"...

image

The result is different because of SwifttUI 2.0? or I'm missing something?

Thanks

Another Question about Navigation/Routing

Hey Alex, thanks a lot for your example application.

I am creating another sample app based on the provided architecture of your example to get a better understanding of SwiftUI and Combine.

I have another problem with the navigation/routing. I basically copied your approach but I have these weird back and forth navigation behaviour, maybe you could take a look at my code. I have been going through the Stackoverflow topics, but I wasn't able to fix it and at the same time keep your routing example.

struct CategoriesList: View {
    @Environment(\.locale) private var locale: Locale
    @Environment(\.injected) private var injected: DIContainer
    @State private var routingState: Routing = .init()
    private var routingBinding: Binding<Routing> {
        print(routingState)
        return $routingState.dispatched(to: injected.appState, \.routing.categories)
    }
    private var categories = Category.allCases
    var body: some View {
        content
            .onReceive(routingUpdate) { value in
                print(value)
                self.routingState = value
        }
    }
    
    
    private var content: some View {
        NavigationView {
            List(self.categories) { category in
                NavigationLink(
                    destination: Exercises(),
                    tag: category.rawValue,
                    selection: self.routingBinding.categories) {
                        CategorieCell(name: category.rawValue)
                }
            }.navigationBarTitle("Categories")
        }
        
        
    }
}

// MARK: - State Updates
private extension CategoriesList {
    var routingUpdate: AnyPublisher<Routing, Never> {
        injected.appState.updates(for: \.routing.categories)
    }
}

extension CategoriesList {
    struct Routing: Equatable {
        var categories: String?
    }
}



struct ExerciseDetail: View {
    var body: some View {
        Text("Hello DetailsView")
    }
}

Link to the file:
https://github.com/Patrick3131/LearnHockey/blob/dev/LearnHockey/UI/Screens/CategoriesList.swift

Thanks!

Question: Deeper than one-level navigation

Hi Alexey,

Having a look at your repo to get ideas on how to tackle my own issues - have you tackled anything to do with navigation (in the NavigationView sense) beyond a one-level-deep-master-detail-set?

I'm still looking for ideas to an SE question I posted - you mentioned in https://nalexn.github.io/swiftui-deep-linking/ that you had this working for programmatically navigat(ing) to a screen at any depth - but I keep on hitting odd animation issues.

Other than that - good to see others are tackling and posting the non-trivial stuff - Apple's documentation and examples are very sorely lacking!

Xcode 12 GM warnings when updating local state from AppState

Not sure how critical this is, I just jumped into Xcode 12 GM today, but I have loads of warnings which were not there with Xcode 11. Once again, not sure it actually affects anything, but maybe worth mentioning.

Basically, all the local state updates which come from the injected AppState updates are marked like this:

image

Maybe it makes sense to use the new onChange(of:perform:) API?

Question: Multiple interactor calls

We have a page with a non persistent data (we don't use AppState for this one).
It displays two types of information, let's say Product details and Company record.

We are fetching Product data through interactor call and binding its value to loadable binding state by onAppear event.

Once we got the product info data that contains company id - we are pulling the company record. Right now we are doing this simply by handling onAppear event on the UI elements that depends on Product record == .loaded(product).

I am wondering how would handle such a case using Combine. It works but I personally feel that that the code smells.

Would love to hear how would you handle such a case. Seeking to make to more cleaner.

Attaching a simple, minimal example: https://gist.github.com/AndriiTsok/e49a90f0aa648314b6ba0d35ef42fca0

ViewModels reinitialize after view changes

First of all, thanks for all the work put onto this project. I'm building a project based off of this and it's looking really good, but I am encountering some issues that I explain below.

To summarize:

ViewA navigates to ViewB. To achieve this, I implement a NavigationLink(destination: ViewB(viewModel: ViewBViewModel())). When inside ViewB, some app state is modified, which ViewA receives with a onReceive. In the onReceive callback, ViewA makes some changes to its view like disabling a button. This causes ViewA to recalculate itself, which initializes ViewBViewModel again, losing all the changes, including all loaded data from the repository, leaving the user on ViewB with all Loadable set to .notRequested, and all viewModel state lost.

This also happens if the app state is implemented through a EnvironmentObject. If I modify some environment object from ViewB that ViewA uses, ViewA recreates itself, losing all the state on ViewBViewModel.

I am also having some issues related to this when navigating back from a view to its predecessor. In some cases, if I navigate from some view ViewA to other view ViewB, when dismissing ViewB, ViewA gets recalculated, which initializes ViewBViewModel again.

Has anyone encountered this behaviour and came to a solution?

Separate Routing/Navigation from View

Hi Alex, as I mentioned before I like your sample project, your thoughts and implementation of SwiftUI and Combine.

I want to separate the View from the navigation flow, therefore I created a separate Router. The router contains the state of the routing or visibility of sheet popups and takes care of creating the destinations, basically NavigationLink vViews.
Therefore the view does not know about the links to the next view.

Implemented my approach looks like this:

extension AccountView {
    class Router: ObservableObject, AccountRouting {
        @Published var routingState: Routing
        @Published var showLoginView = false
        @Published var showManageSubscription = false
        
        private var cancelBag = CancelBag()
        
        init(appState: Store<AppState>) {
            _routingState = .init(initialValue: appState.value.routing.account)
            cancelBag.collect {
                [
                    $routingState
                    .removeDuplicates()
                        .sink { appState[\.routing.account] = $0}
                ]
            }
        
        }
        
        func showLoginView(viewModel: ViewModel) -> AnyView {
            return AnyView(EmptyView()
                .sheet(isPresented: Binding<Bool>.init(get: { self.showLoginView },
                                                       set: { self.showLoginView = $0 }), content: {
                                                        LoginView(cancel: {
                                                            viewModel.cancelLogin()
                                                        })
                }))
        }
        
        func showManageSubscription(viewModel: ViewModel) -> AnyView {
            return AnyView(EmptyView()
                .sheet(isPresented: Binding<Bool>.init(get: { self.showManageSubscription },
                                                       set: { self.showManageSubscription = $0 }), onDismiss: {
                }, content: {
                    BuySubscriptionView(viewModel: viewModel.createBuySubcriptionViewModel())
                })
            )
        }
    }
}

or

extension CategoriesListView {
    class Router: ObservableObject, CategoriesListRouting {
        @Published var exercisesRouting: CategoriesListView.Routing
        private var cancelBag = CancelBag()
        
        init(appState: Store<AppState>) {
            _exercisesRouting = .init(initialValue: appState.value.routing.categories)
            
            self.cancelBag.collect {
                $exercisesRouting
                    .removeDuplicates()
                    .sink { appState[\.routing.categories] = $0 }
                
                appState.map(\.routing.categories)
                    .removeDuplicates()
                    .assign(to: (\.exercisesRouting), on: self)
            }
        }
        
        func exerciseViewDestination(viewModel: CategoriesListView.ViewModel, category: Category) -> AnyView {
            return AnyView(
                NavigationLink(
                    destination: ExercisesView(viewModel:
                        viewModel.createExerciseViewModel(category: category)),
                    tag: category.name,
                    selection: Binding<String?>.init(get: {self.exercisesRouting.categories},
                                                     set: {  self.exercisesRouting.categories = $0 ?? ""}))
                {
                    CategorieCell(name: category.name, number: "\(category.numberOfExercises)")
            })
        }
    }
}

The Router itself is owned by the ViewModel of the View.
The View would call a function of the ViewModel that returns AnyView. The ViewModel then calls a function of the router that would call the NavigationLink wrapped in AnyView.
What do you think, does this approach make sense to you? Do you have any other ideas how to take away routing responsibilities from the View?

Full project: https://github.com/Patrick3131/LearnHockey

Tapping flag results in error

When running the app, if tap on any flag in the country detail, you get the following error:
Thread 1: Simultaneous accesses to 0x7f9ef2eae650, but modification requires exclusive access

Improper view layout after swiping to navigate back to search result

I found that after swiping to navigate back in detail view to search result (with soft keyboard) will lead to improper layout, check the screenshot below:
IMG_2351

I wonder if it's another bug in SwiftUI or not good integration of SearchBar.

Thanks, love this amazing cleaner architecture.

Decoding JSON to Swift Model Not Root Level

I have one case related to decoding JSON, while I have created a model based on the JSON structure which is dictionary but the data that I need is in nested fields with an array type, not at the root level.

Based on the architecture of your application how I can go directly to nested filed to get array because I got the below error once I am trying to parse the actual JSON retrieved from API.

Error:
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))

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?

Question: Dealing with auth tokens using clean architecture

How should I set auth token?
After I authenticate I need to pass auth token in headers for every request.
In your article you briefly mentioned that tokens would be stored in AppState.
But how can I access token stored in state on Repository level (since we set headers over there)

Question: AppLaunch Time and Memory management for Store

First of all, thanks for the great demo.

I have two questions when the App gets bigger and it needs many Services:

  1. You initilaize the store before the app Lauch in AppEnvironment.bootstrap(), wouldnt it have a negative impact in the app launch time.
  2. Once a service or state is created, it will remain in memory in the Store, wouldnt that affect the app performance or take to much memory when the are many services or states? Would it make sense to somehow deinit some of the unused states or services.

Question: AppState Scalability and Encapsulation

It´s difficult for me to see how the AppState is scalable, if you have many screens, and each screen has specific and shared states, it would require that I create nested structs in the AppState to make it more modular, but it will also make it bigger. How maintable and scalable do you think the AppState is?

The previous leads to my next point. In a View you have the store as a property, and if you need a specific nested state you may end up with e.g. store.wallet.payment.cashback.cards. Is there a better way?

Last but not least, each View knows too much about all the states, and I would like that it only knows about the specific states it needs. How would you adress this?

Routing state consistency

First of all I would like to say "Thank you" for this great example!

I have a question regarding routing state consistency - when app opens country details it setups injected.appState[\.routing.countriesList.countryDetails] = #countryId#, but when user clicks Back button, I think, we need to clean routing value. I found the function goBack(), but I don't see any callers.

Could you please clarify this?

Question: UserDefaults

How would you deal with UserDefaults?, usually I'd use an ObservableObject.
But would it be preferable for the status of preferences to be in the store?

I've tried adding an interactor that has UserDefaults backed properties, but then they aren't bindable directly I guess it would have to work like routing?
Interactor => [Subscribes to UserDefaults changes and pushed to Store]
Then you'd need something like routingBinding but sending changes via the interactor?

Based on #30 for convenience would you just use a ObservableObject? and if so would you create an instance per view rather than storing it in EnvironmentObjects?

Chain one or more calls on completion of the first

Hi @nalexn and thank you for providing your knowledge regarding Swift and SwiftUI! Really awesome work.

I have a question regarding chaining of multiple requests on completion of the first. I've looked into #21 and you provided a solution there (different interactors though, in my case it's multiple endpoint of the same WebRepository, and I'm using the MVVM variant of this repo).
Instead of using a LoadableSubject I would like to populate two or more Loadables in the AppState in a single "transaction", but since the first one will get a token and the second one will need this token to authenticate, it's imperative that the first call, getting and setting the token, is completed before the second one is fired.

Also, when trying to put a Loadable<(Token, User)>, with a tuple as is done in your example in #21, but there with a LoadableSubject instead, the AppState no longer complies with the Equatable protocol (both of the Codable models in the tuple are Equatable though). Why is this?

Is there another way of doing this chaining process which is in line with your philosophy? Or is it preferred that the UI layer chains the requests instead of the service or repository layer? (It seems wrong to me. I will always have to get the User after the Token is fetched, and also a few other requests that depends on another).

Best regards,
Jens

cancel() in CancelBag

I noticed there's a method of cancel in CancelBag. When to use this method? (I can not find a case in your project)

Question: navigate back programmatically

First of all, thank you for the awesome architecture!

I encountered following problem, when trying to extend it with following scenario:

Supposing the CountryDetailsView has some kind of update button, e.g.:

func basicInfoSectionView(country: Country, details: Country.Details) -> some View {
    Section(header: Text("Basic Info")) {
        ...
        Button(action: viewModel.updateCountry) {
            Text("Update")
        }
    }
}

The CountryDetailsViewModel consists now of following function:

func updateCountry() {
    // do some update stuff
    container.appState[\.routing.countriesList].countryDetails = nil
}

So after 'updating' the country (in my case doing an api call),
i want the detail view to pop.
But doing so programmatically with the help of the appState doesn't work.

I tried changing the NavigationLink in CountriesList to work with isActive, since the selection version has knowingly some bugs (https://stackoverflow.com/questions/66128198/swiftui-list-selection-always-nil) but it's still not working.

The only way i've managed the view to pop is by injecting:

  @Environment(\.presentationMode) var presentationMode

into the view but then only the view can pop itself not the viewModel.

Do you have some idea how i could implement it the clean way?

Обновление Loadable-объектов. Возможно ли?

Во-первых хочу поблагодарить за проделанную работу. Проект получился очень показательным и в крайней степени полезным. Мне очень понравилась данная архитектура и хотелось бы использовать её во всех своих проектах (SwiftUI) в будущем.

Не могу сказать, что обладаю должным опытом, так что вопрос может показаться довольно глупым, но всё же.

Правильно я понимаю, что Loadable-объекты, используемые для разделения каждого View на несколько статусов отображения, созданы только для чтения, но не для записи?

Как, в таком случае, обновлять объекты отображаемые на экране благодаря данным из Loadable-объекта?

Заранее спасибо.

Ps. Проше прощения что не на английском. Так было проще =)

Question: clean architecture, boundaries and reactive frameworks

Hello !

I just discovered your work through this breakdown https://nalexn.github.io/uikit-switfui/ and followed my way up to here :). First of all thank you for this very well articulated article. While reading it, I was thinking "yes.. yes... oh yeah ! been there, see what you mean... totally agree and very-well phrased... this guy is on point !"
I especially loved this part:
"Nevertheless, there is a heck of a lot of business logic running on modern mobile apps, but that logic is different. It’s just more focused on the presentation rather than on the core rules the business runs on.

This means we need to do a better job at decoupling that presentation-related business logic from the specifics of the UI framework we’re using."

Still, one thing "surprised"/"puzzled" me a bit, regarding clean architecture. On one hand you seem very prone to raise a strict boundary between the presentation layer & the "domain/core layer" (even for simple use cases that fetch data from an abstract repo), pushing UIKit / SwiftUI specificities to the outer limits, where they belong. Basically, if it's a framework then I don't want it to be polluting the core of my app.
Yet, you don't seem reluctant or bothered at all to "let" a reactive programming framework (be it an Apple "stamped" one like Combine or worse, a third-party one like Rx) going through pretty much everything, from data fetching to use-cases to presentation to ui. I'm sure this is a totally deliberate choice so I'd be glad if you can share some thoughts on that. For instance, did you ever considered preserving your core as strictly 100% vanilla Swift and plug the rest of the system through reactive adapters to this core ? Do you feel it's not worth it ?
In a fictive scenario where Apple's Combine isn't quite "bridgeable" to Rx, because their respective philosophy differs a bit too much, this could be very damaging, don't you think ?

test notification opens current detail instead of the payload one then crashes

When dragging the notification file to the simulator, routing is inconsistent.

Example:

Let app be in the country list screen
drag notification file to simulator
app routes to Andorra's flag detail ✅

Let app be in Belgium detail
drag notification file to simulator
app routes to Belgium's flag detail
[Crash] when user taps close on Belgium's flag detail❌

CountriesSwiftUI[7277:216049] Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c9e0f5c22SheetHostingControllerVS_7AnyView_: 0x7fa0dd8638b0>  on <_TtGC7SwiftUIP13$7fff2c9eec3028DestinationHostingControllerVS_7AnyView_: 0x7fa0dd947b60> which is already presenting (null)
(lldb) 

Xcode: 11.6

Image caching

Hi!

Some time ago, you've swapped your own implementation of file/in memory caching in favour of URLSessionConfiguration caching.

The latter approach does not work that well in some cases. Say, the image was missing at some specific URL (maybe the server was down), the data is returned (for instance, it was an error page) and is cached. This data couldn't be converted to image (UIImage(data: data)), still, every next load invocation will have wrong cached data and won't try to check if there is a valid data present.

Maybe it makes sense to manage the same URLCache but manually using CachedURLResponse, instead of using .returnCacheDataElseLoad?

Countries not loading with error in console on iOS 13.3 simulator

On iOS 13.3 app is not working. Infinite loading indicator with error in console

2020-01-27 16:36:27.686086+0200 CountriesSwiftUI[93607:5277210] 
Task <CF46B85F-26FE-4D81-917C-338E10956D73>.<1> 
finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" 
UserInfo={NSErrorFailingURLStringKey=https://restcountries.eu/rest/v2/all, 
NSLocalizedDescription=cancelled, NSErrorFailingURLKey=https://restcountries.eu/rest/v2/all}

Blog Suggestion: CoreData, Clean Architecture, Combine and SwiftUI

Would you, at some point, write a blog post about your methods about,
how to integrate the Core Data (with CloudKit-enabled) into Clean Architecture using Combine;
and how could we from the SwiftUI update the data source, while maintaining their synchronous.

Like, how would you bridging the Data Layer(which is struct based) and the Core Data(which is class of NSManagedObject); Will the same data be duplicated? How to manage the Fault for relationships of the CoreData? How to reduce the amount of work when updating a large list, do we actually rebuild the entire list or just partial of the list? etc.

I've found in your existing code some great implementations, though would love to read more about it.

Many thanks!

Question: External events

How would you add a BLE peripheral manager to this architecture?

Seems like it would be a handler but where would the business logic of responding the events go?

Question: infinite scroll

I found your repository and architecture like Redux is quite new for me.
I wonder how to use your architecture with infinite scroll.
Imagine your application with only network call. How will you handle infinite scroll with your architecture? Where do you store the current list of country + the fresh new one from the server? How would you use your loadable class in such architecture for this kind of use case?
Thanks

[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.

bgQueue is unused in WebRepository

Hello, I've just started digging in this project and I found that the bgQueue defined in WebRepository isn't used.

Is there something I'm missing or it isnt really necessary to define bgQueue in the protocol?

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!

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.