├── .github └── FUNDING.yml ├── SuperSimple.png ├── LICENSE.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ochococo 4 | -------------------------------------------------------------------------------- /SuperSimple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ochococo/Super-Simple-Architecture/HEAD/SuperSimple.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Oktawian Chojnacki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Super Simple Architecture](SuperSimple.png) 2 | 3 | ![Swift5](https://img.shields.io/badge/%20in-swift%205.0-orange.svg) 4 | 5 | # Super Simple 6 | 7 | ### Application Architecture you can learn in minutes. 8 | 9 | ## Why? 10 | 11 | > Nobody is really smart enough to program computers. 12 | > 13 | > Steve McConnell (Code Complete 2.0) 14 | 15 | We believe we should compose software from components that are: 16 | 17 | - accurately named, 18 | - simple, 19 | - small, 20 | - responsible for one thing, 21 | - reusable. 22 | 23 | # Components 24 | 25 | ## Types: 26 | 27 | - Active Components (ex. Interaction, Navigation, Mediator etc.) 28 | - Passive Components (ex. Renderable) 29 | - Views (ex. UILabel) 30 | 31 | ## 🖼 Renderable 32 | 33 | > Renderable 34 | > (*noun*) 35 | > 36 | > Something (information) that view is able to show. 37 | 38 | ### Definitions 39 | 40 | ### TL;DR 41 | 42 | Renderable is a struct that you pass to a View which should show its contents. 43 | 44 | #### Renderable is: 45 | - Passive, 46 | - immutable struct, 47 | - often converted from model or models, 48 | 49 | ```swift 50 | struct BannerRenderable { 51 | let message: String 52 | } 53 | ``` 54 | 55 | #### View should: 56 | - always look the same if ordered to render the same renderable, 57 | - have code answering to one question: "how should I look?", 58 | - have no state, 59 | - accept infinite number of renderables in random order. 60 | 61 | ### Example 62 | 63 | **Step 1 - Create Renderable:** 64 | 65 | ```swift 66 | struct BannerRenderable { 67 | let message: String 68 | 69 | init(message: String) { 70 | self.message = message 71 | } 72 | } 73 | ``` 74 | 75 | **Step 2 - Expose View ability in Protocol:** 76 | 77 | ```swift 78 | protocol BannerRendering: AnyObject { 79 | func render(_ renderable: BannerRenderable) 80 | } 81 | ``` 82 | 83 | **Step 2 - Implement View:** 84 | 85 | ```swift 86 | final class BannerView: UIView { 87 | @IBOutlet fileprivate var bannerLabel: UILabel! 88 | } 89 | 90 | extension BannerView: BannerRendering { 91 | func render(_ renderable: BannerRenderable) { 92 | bannerLabel.text = renderable.message 93 | } 94 | } 95 | ``` 96 | 97 | ## 🛰 Interaction 98 | 99 | > Interaction 100 | > (*noun*) 101 | > 102 | > Component answering the question: What should I do in reaction to user's action. 103 | 104 | ### Definitions 105 | 106 | ### TL;DR 107 | 108 | Interaction is a final class that ViewController or View owns, which performs actions in reaction to user input - gestures, text input, device movement, geographical location change etc. 109 | 110 | #### Interaction: 111 | - Active component (has a lifecycle), 112 | - final class, 113 | - can use other components for data retrieval, 114 | - should be literally between two actions, the one causing and the one caused, 115 | - can call navigation methods when other ViewController should be presented, 116 | - can call render methods on weakly held views (seen via protocol). 117 | 118 | 📌 RULE: There is no good reason for inheritance of custom classes, ever. 119 | 120 | #### ViewControllers and Views: 121 | 122 | - Can have many Interactions for separate functions, 123 | - should OWN Interaction and see it via protocol, 124 | - subviews of VC could own separate Interaction but it’s not mandatory for simple VC. 125 | 126 | ### Example 127 | 128 | **Step 1 - Create Interaction protocols:** 129 | 130 | ```swift 131 | protocol PodBayDoorsInteracting: AnyObject { 132 | func use(_ banner: BannerRendering) 133 | func didTapMainButton() 134 | } 135 | ``` 136 | 137 | **Step 2 - Add Interaction implementation:** 138 | 139 | 📌 RULE: Do not tell Interaction what to do, tell what happened, it should decide what should happen next. 140 | 141 | ```swift 142 | final class PodBayDoorsInteraction { 143 | fileprivate let killDave = true 144 | fileprivate weak var banner: BannerRendering? 145 | } 146 | 147 | extension PodBayDoorsInteraction: PodBayDoorsInteracting { 148 | private enum Strings { 149 | static let halsAnswer = "I know you and Frank were planning to disconnect me, and that is something I cannot allow to happen." 150 | } 151 | 152 | func didTapMainButton() { 153 | guard let banner = banner else { fatalError() } 154 | 155 | if killDave { // Just to show the business logic is resolved here. 156 | banner.render(BannerRenderable(message: Strings.halsAnswer)) 157 | } else { 158 | // Open doors. Not implemented :P 159 | } 160 | } 161 | 162 | func use(_ banner: BannerRendering) { 163 | self.banner = banner 164 | } 165 | } 166 | ``` 167 | 168 | **Step 3 - Use it in ViewController (via nib):** 169 | 170 | ```swift 171 | final class DiscoveryOneViewController: UIViewController { 172 | 173 | fileprivate let podBayInteraction: PodBayDoorsInteracting 174 | 175 | @IBOutlet private var bannerView: BannerRendering! 176 | 177 | init(podBayInteraction: PodBayDoorsInteracting) { 178 | self.podBayInteraction = podBayInteraction 179 | super.init(nibName: nil, bundle: nil) 180 | } 181 | 182 | override func awakeFromNib() { 183 | super.awakeFromNib() 184 | podBayInteraction.use(bannerView) 185 | } 186 | 187 | @IBAction private func didTapMainButton() { 188 | podBayInteraction.didTapMainButton() 189 | } 190 | } 191 | ``` 192 | 193 | ## 🛠 Assembler 194 | 195 | > Assembler 196 | > (*noun*) 197 | > 198 | > Assembler is gathering dependencies and injecting them into a single instantiated component. 199 | 200 | ### Definitions 201 | 202 | ### TL;DR 203 | 204 | Its purpose is to assemble dependencies for one class or struct. 205 | 206 | #### Assembler: 207 | - an enum, 208 | - has only one method called `assemble()`. 209 | - can call ONLY ONE initializer directly, 210 | - will call other Assemblers to instantiate dependencies. 211 | 212 | ### Example 213 | 214 | **Step 1 - Create Interaction protocols:** 215 | 216 | 📌 RULE: When assembled component is dependent on other instances, you should use it's Assemblers, do not initialize more than one class directly. 217 | 218 | ```swift 219 | protocol PodBayDoorsInteractionAssembling { 220 | func assemble() -> PodBayDoorsInteracting 221 | } 222 | 223 | struct PodBayDoorsInteractionAssembler { 224 | func assemble() -> PodBayDoorsInteracting { 225 | return PodBayDoorsInteraction() 226 | } 227 | } 228 | 229 | protocol DiscoveryOneAssembling { 230 | func assemble() -> UIViewController 231 | } 232 | 233 | struct DiscoveryOneAssembler: DiscoveryOneAssembling { 234 | 235 | let interactionAssembler: PodBayDoorsInteractionAssembling 236 | 237 | func assemble() -> UIViewController { 238 | return DiscoveryOneViewController(interaction: interactionAssembler.assemble()) 239 | } 240 | } 241 | 242 | let interactionAssembler = PodBayDoorsInteractionAssembler() 243 | let viewControllerAssembler = DiscoveryOneAssembler(interactionAssembler: interactionAssembler) 244 | let viewController = viewControllerAssembler.assemble() 245 | ``` 246 | ## 🧭 Navigation 247 | 248 | > **Navigation** 249 | > (*noun*) 250 | > 251 | > Component responsible for presenting views (here: View Controllers). 252 | 253 | ### Definitions 254 | 255 | ### TL;DR 256 | 257 | Interaction may need to open another View Controller in response to user's action, it has to use Navigation component to do that. 258 | 259 | #### Navigation is: 260 | - A class, 261 | - owned by Interaction, 262 | - can push or open views modally, 263 | - will use Assemblers to create ViewControllers to show. 264 | 265 | ### Example 266 | 267 | 📌 RULE: We should never pass classes directly, always via protocol or Adapter. 268 | 269 | **NavigationControlling.swift** 270 | ```swift 271 | protocol NavigationControlling: AnyObject { 272 | func pushViewController(_ viewController: UIViewController, animated: Bool) 273 | } 274 | 275 | // `NavigationControlling` is a protocol convering (some) methods of `UINavigationController` 276 | extension UINavigationController: NavigationControlling { } 277 | ``` 278 | 279 | **Step 1 - Create Navigating protocol:** 280 | 281 | 📌 RULE: When A uses B but A is not an owner of B, pass B via `use(_)` method, not in `init`. 282 | 283 | **PodBayNavigation.swift** 284 | ```swift 285 | protocol DiscoveryOneNavigating: AnyObject { 286 | func use(_ navigationController: NavigationControlling) 287 | func presentDiscoveryOneInterface() 288 | } 289 | ``` 290 | 291 | **Step 2 - Implement Navigation:** 292 | 293 | **DiscoveryOneNavigation.swift** 294 | ```swift 295 | final class DiscoveryOneNavigation: DiscoveryOneNavigating { 296 | 297 | private let discoveryAssembler: DiscoveryOneAssembling 298 | private weak var navigationController: NavigationControlling? 299 | 300 | init(navigationController: NavigationControlling, discoveryAssembler: DiscoveryOneAssembling) { 301 | self.navigationController = navigationController 302 | self.discoveryAssembler = discoveryAssembler 303 | } 304 | 305 | func use(_ navigationController: NavigationControlling) { 306 | self.navigationController = navigationController 307 | } 308 | 309 | func presentDiscoveryOneInterface() { 310 | let discovery = discoveryAssembler.assemble() 311 | navigationController.pushViewController(discovery, animated: true) 312 | } 313 | } 314 | ``` 315 | --------------------------------------------------------------------------------