{"id":19509862,"url":"https://github.com/steamclock/nicearchitecture","last_synced_at":"2025-04-15T19:38:53.234Z","repository":{"id":182606315,"uuid":"536758370","full_name":"steamclock/NiceArchitecture","owner":"steamclock","description":null,"archived":false,"fork":false,"pushed_at":"2024-11-06T21:42:38.000Z","size":774,"stargazers_count":1,"open_issues_count":7,"forks_count":2,"subscribers_count":11,"default_branch":"main","last_synced_at":"2024-11-10T23:13:34.285Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/steamclock.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-14T21:01:41.000Z","updated_at":"2024-08-08T08:00:48.000Z","dependencies_parsed_at":null,"dependency_job_id":"969123f0-7af9-45ed-9c96-e9e545e0280b","html_url":"https://github.com/steamclock/NiceArchitecture","commit_stats":null,"previous_names":["steamclock/steamclutility-belt"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2FNiceArchitecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2FNiceArchitecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2FNiceArchitecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2FNiceArchitecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/steamclock","download_url":"https://codeload.github.com/steamclock/NiceArchitecture/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233026415,"owners_count":18613580,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-10T23:13:37.574Z","updated_at":"2025-01-08T11:41:55.539Z","avatar_url":"https://github.com/steamclock.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![](nice-header.png)\n\n# NiceArchitecture\n\nThis repository is intended to be used as a more detailed reference for how we like to architect SwiftUI apps at Steamclock. [This blog post]()(coming soon™) goes over the architecture at a higher level - including goals, motivations, et cetera, while this repo digs into the specifics of how you might implement NiceArchitecture in the wild.\n\nFor the most part, NiceArchitecture sticks with the standard MVVM concepts most mobile developers are familiar with like ViewModels, Repositories, and Services, but adds in a little spice with the concept of ViewCoordinators to handle navigation, and opinions on things like Dependency Injection and how to manage a screen’s load state.\n\nAdditionally, we've included a package (also called NiceArchitecture) that provides a bunch of tools and helpers that we've found useful when developing apps under this architecture. You can read more about its contents and what they do below, or check out the [example project](https://github.com/steamclock/NiceArchitecture/tree/main/NiceArchitectureExample/NiceArchitectureExample) to see how they work in context.\n\n#### Index\n - [The Example Project](#the-example-project)\n - [ContentLoadState](#contentloadstate)\n - [ObservableVM](#observablevm)\n - [Stateful View](#stateful-view)\n - [Dependency Injection](#dependency-injection)\n - [Error Handling](#error-handling)\n - [Cacheing](#cacheing)\n - [Array+Cancellable](#array+cancellable)\n\n## The Example Project\n\nThe example project contained in this repository outlines how we like to architect SwiftUI apps as of December 2023. For more context, you should probably read the accompanying blog post (coming soon™) before digging in here.\n\nOnce you're up to speed, it's probably best to get started in the PostsCoordinatorView (TODO: Link), then dive into the individual Views and their ViewModels from there. Rather than including more documentation for individual classes here, we've opted to include that information in-line in the example project, to give you a better idea of how things fit together in context. \n\n## ContentLoadState\n\nWhen managing Views, we frequently want to make sure that the state of the View matches whatever's happening in the background, and want this to be consistent across each View. To do this, we use a simple enum that contains the most important states a View may be in:\n\n- Loading: The content is currently loading\n- HasData: The content has loaded successfully\n- NoData: The content has loaded successfully, but is empty.\n- Error: Something's gone wrong\n\nTo see this in action, check out the [PostsView](https://github.com/steamclock/NiceArchitecture/blob/main/NiceArchitectureExample/NiceArchitectureExample/UI/Posts/PostsView.swift).\n\n## ObservableVM\n\nA lot of our ViewModels end up needing to share a lot of the same behaviour, like keeping track of their View's ContentLoadState, binding to Views, handling errors, managing Cancellables, etc. By extending ObservableVM, our ViewModels get a lot of that behaviour automatically, which also helps us make sure we don't forget any of the pieces when adding new ViewModels.\n\nTo see a detailed example, check out the [PostsViewModel](https://github.com/steamclock/NiceArchitecture/blob/main/NiceArchitectureExample/NiceArchitectureExample/UI/Posts/PostsViewModel.swift).\n\n## StatefulView\n\nMuch like ObservableVM provides a starting point for writing new ViewModels, StatefulView provides a starting point for new Views that are bound to ObservableVMs. StatefulView allows a View to dynamically track its ContentLoadState and update appropriately. It also includes default states for the loading, error and noData states.\n\n[PostsView](https://github.com/steamclock/NiceArchitecture/blob/main/NiceArchitectureExample/NiceArchitectureExample/UI/Posts/PostsView.swift) contains a more detailed example of how to use this.\n\n## Dependency Injection\n\nIn the interest of writing more modular, testable, code we recommend providing repositories and services through depenency injection, rather than creating global variables or singletons.\n\nFirst, create a protocol of the class to be injected and an injection key for it. We do this instead of creating these on the class directly to allow for mocking in tests.\n```\nprotocol UserServiceProtocol {\n    func getCurrentUser(id: String) async throws -\u003e User\n    func updateUserEmail(id: String, email: String) async throws -\u003e Bool\n}\n\npublic struct UserServiceKey: InjectionKey {\n    public static var currentValue = UserServiceProtocol()\n}\n```\n\nThen, add your new service to the `InjectedValues`:\n```\nextension InjectedValues {\n    var userService: UserServiceProtocol {\n        get { Self[UserServiceKey.self] }\n        set { Self[UserServiceKey.self] = newValue }\n    } \n    \n    // ...\n}\n```\n\nNow you can create your fill in your user service class, and inject it into view models to use:\n\n```\nclass AViewModel: ObservableViewModel {\n    @Injected(\\.userService) private var userService: UserServiceProtocol\n```\n\n## Error Handling\n\nIncluded in this library in a ready-to-be-injected class called `ErrorService`, designed to receive incoming errors through the `error` Subject.\n\nView models can listen to `didReceiveDisplayableError` and handle the results as needed.\n\nWe use 3 different protocols to organize and filter errors as they're passed through via the `ErrorService`:\n\n#### Displayable Error\n\nDisplayable errors are ones meant to be shown to the user, either as an alert or in-line.\n\n```\nenum CreateAccountError: DisplayableError {\n    case emailTaken\n    case invalidPassword\n    \n    var title: String {\n       switch self {\n       case .emailTaken:\n           return \"Email Address Already Taken\"\n       case .invalidPassword:\n           return \"Invalid Password\"\n       }\n   }\n\n   var message: String {\n       switch self {\n       case .emailTaken:\n           return \"An account with that email already exists, try logging in?\"\n       case .invalidPassword:\n           return \"Your password must contain 8 letters, a capital letter, an emoji, a mathematical equation, your birth sign, and favourite hobbit.\"\n       }\n   }\n}\n\n```\n\n#### Loggable Error\n\nAn error that is meant to generate a log message.\n\n```\nenum ApiError: LoggableError {\n    case decoding(String)\n    \n    var typeDescription: StaticString {\n        switch self {\n        case .decoding:\n            return \"Decoding Error\"\n        }\n    }\n\n    var errorDescription: String {\n        switch self {\n        case .decoding(let message):\n            return message\n        }\n    }\n}\n```\n\n#### Suppressible Error\n\nAn error that may not be logged or shown to the user.\n\n```\nenum UserEntryError: SuppressibleError {\n    case wrongPassword\n    case invalidEntry\n    \n    var shouldDisplay: Bool {\n        switch self {\n        case .wrongPassword: return true\n        case .invalidEntry: return false\n        }\n    }\n}\n\n```\n\nIn addition, we provide two common error types: \n\n- `ConnectivityError` when the app is unable to connect to the internet\n- `UnknownError` for when you're all out of other errors\n\n## Cacheing\n\nThe provided `CacheService` allows Repositories to create and manage their own caches. While you could inject a shared cache into each repository, we recommend creating a separate cache for each repository unless you've got a good reason not to.\n\nAlternatively, you can use CurrentValueSubjects in your Repositories to handle caching for you.\n\n## Array+Cancellable\n\nOur view models tend to whole a bunch of bindings in an array that we want to clear efficiently when we unbind the view model, this makes that quick and easy.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteamclock%2Fnicearchitecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsteamclock%2Fnicearchitecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteamclock%2Fnicearchitecture/lists"}