├── .github └── workflows │ ├── deploy-docs.yml │ └── nef-compile.yml ├── .gitignore ├── BowArch.podspec ├── Documentation.app ├── Contents │ ├── Info.plist │ ├── MacOS │ │ ├── .gitignore │ │ ├── Background.playground │ │ │ ├── Pages │ │ │ │ ├── Comonadic UIs.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ ├── Monads and Comonads.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ ├── Overview.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ └── Pairings.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ ├── contents.xcplayground │ │ │ └── playground.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ ├── Core concepts.playground │ │ │ ├── Pages │ │ │ │ ├── Component.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ ├── Dispatcher.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ ├── State and Input.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ └── View.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ ├── contents.xcplayground │ │ │ └── playground.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ ├── Documentation.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Documentation.xcscheme │ │ ├── Documentation.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Documentation │ │ │ └── Info.plist │ │ ├── Legal.playground │ │ │ ├── Pages │ │ │ │ ├── Credits.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ └── License.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ ├── contents.xcplayground │ │ │ └── playground.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ ├── Patterns.playground │ │ │ ├── Pages │ │ │ │ ├── Creating a single component.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ └── Creating nested components.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ ├── contents.xcplayground │ │ │ └── playground.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ ├── Podfile │ │ ├── Quick start.playground │ │ │ ├── Pages │ │ │ │ ├── Getting started.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ │ └── Resources.xcplaygroundpage │ │ │ │ │ └── Contents.swift │ │ │ ├── contents.xcplayground │ │ │ └── playground.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ └── launcher │ └── Resources │ │ ├── AppIcon.icns │ │ └── Assets.car └── Jekyll │ └── Home.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── BowArch │ ├── ActionMoore │ ├── ActionDispatcher.swift │ ├── ActionHandler.swift │ └── MooreComponent.swift │ ├── Core │ ├── Component.swift │ ├── ComponentView.swift │ ├── Dispatcher.swift │ ├── Handler.swift │ ├── Reducer.swift │ └── UI.swift │ ├── IO │ └── Arch+IO.swift │ ├── StateStore │ ├── StateDispatcher.swift │ ├── StateHandler.swift │ └── StoreComponent.swift │ └── WriterTraced │ ├── TracedComponent.swift │ ├── WriterDispatcher.swift │ └── WriterHandler.swift ├── Tests └── BowArchTests │ ├── BowArchTests.swift │ ├── DispatcherTests.swift │ └── HandlerTests.swift ├── assets └── header-bow-arch.png └── docs ├── CNAME ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _data ├── features.yml └── menu.yml ├── _includes ├── _doc.html ├── _footer.html ├── _head-docs.html ├── _head.html ├── _header.html ├── _main.html ├── _nav.html └── _sidebar.html ├── _layouts ├── docs.html └── home.html ├── _sass ├── base │ ├── _base.scss │ ├── _helpers.scss │ └── _reset.scss ├── components │ ├── _button.scss │ ├── _code.scss │ ├── _doc.scss │ ├── _footer.scss │ ├── _header.scss │ ├── _main.scss │ ├── _nav.scss │ ├── _sidebar-menu.scss │ ├── _sidebar.scss │ └── _table.scss ├── utils │ ├── _mixins.scss │ └── _variables.scss └── vendors │ └── highlight │ └── dracula.scss ├── css ├── docs.scss └── styles.scss ├── img ├── bow-arch-brand.svg ├── bow-arch-composable.svg ├── bow-arch-declarative.svg ├── bow-arch-jumbotron-image.svg ├── bow-arch-modular.svg ├── bow-arch-testable.svg ├── favicon.png ├── nav-icon-close.svg ├── nav-icon-open.svg ├── poster.png ├── sidebar-bullet-active.svg ├── sidebar-bullet.svg └── sidebar-icon-open.svg ├── index.md └── js ├── docs.js └── main.js /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Switch Xcode version 15 | run: sudo xcode-select -s /Applications/Xcode_11.4.1.app/Contents/Developer 16 | - name: Generate microsite 17 | run: | 18 | brew install nef 19 | brew install sourcekitten 20 | gem install bundler -v 2.0.2 21 | gem install cocoapods -v 1.9.1 22 | bundle install --gemfile docs/Gemfile --path vendor/bundle 23 | nef jekyll --project Documentation.app --output docs --main-page Documentation.app/Jekyll/Home.md 24 | BUNDLE_GEMFILE=./docs/Gemfile bundle exec jekyll build -s docs -d gen-docs 25 | - name: Deploy microsite 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | personal_token: ${{ secrets.DEPLOY_TOKEN }} 29 | publish_branch: gh-pages 30 | publish_dir: ./gen-docs 31 | disable_nojekyll: true 32 | -------------------------------------------------------------------------------- /.github/workflows/nef-compile.yml: -------------------------------------------------------------------------------- 1 | name: nef verify documentation 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Switch Xcode version 13 | run: sudo xcode-select -s /Applications/Xcode_11.4.1.app/Contents/Developer 14 | - name: Compile documentation 15 | run: | 16 | brew install nef 17 | gem install cocoapods -v 1.9.1 18 | nef compile --project Documentation.app 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | Carthage/ 16 | 17 | ## Other 18 | *.xccheckout 19 | *.moved-aside 20 | *.xcuserstate 21 | *.xcscmblueprint 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | 27 | # Swift Package Manager 28 | .build/ 29 | .swiftpm/ 30 | *.xcodeproj/ 31 | *.plist 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | Pods/ 40 | 41 | Bow\.xcodeproj/project\.xcworkspace/xcshareddata/IDEWorkspaceChecks\.plist 42 | 43 | ## Docs 44 | /docs/*/api-docs 45 | /docs-json 46 | 47 | ## Jekyll 48 | _site 49 | .sass-cache 50 | .jekyll-metadata 51 | 52 | ## OSX generated 53 | .DS_Store 54 | 55 | ## Ruby environment normalization: 56 | /docs/.bundle/ 57 | /docs/vendor/ 58 | /lib/bundler/man/ 59 | docs/docs/* 60 | /docs/_data/sidebar.yml 61 | 62 | ## nef 63 | **/*/nef 64 | 65 | ## Needed due to a bug in GH Pages Travis/Jekyll process 66 | **/*/ffitarget.h 67 | 68 | ## bundle specific 69 | vendor/ 70 | 71 | # fastlane specific 72 | **/fastlane/report.xml 73 | 74 | ## Jekyll 75 | _site 76 | .sass-cache 77 | .jekyll-metadata 78 | .jekyll-cache 79 | -------------------------------------------------------------------------------- /BowArch.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "BowArch" 3 | s.version = "0.1.0" 4 | s.summary = "BowArch is a library to architect SwiftUI-based apps in a functional way." 5 | s.homepage = "https://github.com/bow-swift/bow-arch" 6 | s.license = { :type => 'Apache License, Version 2.0', :text => <<-LICENSE 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | LICENSE 17 | } 18 | s.authors = "The Bow authors" 19 | 20 | s.requires_arc = true 21 | s.osx.deployment_target = "10.15" 22 | s.ios.deployment_target = "13.0" 23 | #s.tvos.deployment_target = "9.1" 24 | #s.watchos.deployment_target = "2.0" 25 | s.source = { :git => "https://github.com/bow-swift/bow-arch.git", :tag => "#{s.version}" } 26 | s.source_files = "Sources/BowArch/**/*.swift" 27 | s.dependency "Bow", "~> 0.8.0" 28 | s.dependency "BowEffects", "~> 0.8.0" 29 | s.dependency "BowOptics", "~> 0.8.0" 30 | s.swift_versions = ["5.2"] 31 | end 32 | -------------------------------------------------------------------------------- /Documentation.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | launcher 9 | CFBundleIconFile 10 | AppIcon 11 | CFBundleIconName 12 | AppIcon 13 | CFBundleIdentifier 14 | com.fortysevendeg.nef 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleSupportedPlatforms 18 | 19 | MacOSX 20 | 21 | LSApplicationCategoryType 22 | public.app-category.developer-tools 23 | LSMinimumSystemVersion 24 | 10.14 25 | NSHumanReadableCopyright 26 | Copyright © 2019 The nef Authors. All rights reserved. 27 | 28 | 29 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/.gitignore: -------------------------------------------------------------------------------- 1 | ## gitignore nef files 2 | **/build/ 3 | **/nef/ 4 | LICENSE 5 | 6 | ## User data 7 | **/xcuserdata/ 8 | podfile.lock 9 | **.DS_Store 10 | 11 | ## Obj-C/Swift specific 12 | *.hmap 13 | *.ipa 14 | *.dSYM.zip 15 | *.dSYM 16 | 17 | ## CocoaPods 18 | **Pods** 19 | 20 | ## Carthage 21 | **Carthage** 22 | 23 | ## SPM 24 | .build 25 | .swiftpm 26 | swiftpm 27 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Background.playground/Pages/Monads and Comonads.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Monads and Comonads 5 | */ 6 | // nef:end 7 | /*: 8 | # Monads and Comonads 9 | 10 | Monads are probably the most dreaded concept by newcomers to Functional Programming. They have a lesser known counterpart, Comonads, which happen to have a very special relationship with Monads. This page does not aim to be a tutorial on Monads and Comonads; rather, we will try to build an intuition of what they are, by reading the operations included in these type classes, and the types involved in them. 11 | 12 | ## Monads 13 | 14 | In Category Theory, Monads are defined as *Monoids in the Category of Endofunctors*. This definition, although correct, is mostly useless in software development. What we need to consider is what are the requirements for something to behave as a Monad. 15 | 16 | First thing we need to point out is that Monads cannot be represented as an abstraction in Swift; the Monad type class is an abstraction that works at a Higher Kind level. That is, it must be conformed by a type `F`, for all `A`. Unfortunately, this is not possible to be expressed in Swift. Bow provides an emulation of Higher Kinded Types that will let us describe this abstraction. 17 | 18 | Then, in programming, the Monad type class requires the implementation of two functions: 19 | 20 | - `pure` or `return`: `(A) -> F` 21 | - `flatMap`, `bind` or `>>=`: `(F, (A) -> F) -> F` 22 | 23 | In ocasions, instead of `flatMap`, you could implement: 24 | 25 | - `flatten`: `(F>) -> F` 26 | 27 | `flatMap` and `flatten` can be implemented in terms of each other; therefore, in order to have an implementation of a Monad, you must provide implementations of `pure`, and `flatMap` or `flatten`. In Bow, you will always be required to implement `pure` and `flatMap`. 28 | 29 | Let's look at the types of each required function. `pure` is a function `(A) -> F`. That is, given any value, the `pure` function can lift it to the context of the `Monad`. In this sense, we can say that monadic operations "introduce context". 30 | 31 | `flatMap` is a function `(F, (A) -> F) -> F`. It has two arguments: `F`, which we can read as "a value in the context of the Monad"; and a function `(A) -> F`, which we can read as "a function to produce a new value in the context of the Monad". Looking at the return type, `F`, the only way we can obtain it is by running the function provided as an argument, but to do so, we need a value of type `A`. We can somehow obtain an `A` from the first argument, given that it exists in the context of the Monad. Therefore, intuitively, the `flatMap` operation lets us perform two effects sequentially, when the second (`F`) depends on the first (`F`). In this way, we can say that Monads let us "chain dependent effects sequentially". 32 | 33 | In summary, from this intuition we can say that Monads introduce context in the operations they are involved, and let us chain effects sequentially. 34 | 35 | ### Example 36 | 37 | One example of a Monad that is pervasively used throughout the library is State. `State` represents a function `(S) -> (S, A)`; that is, a function that receives a value of the state model, and produces a tuple with a modification of the provided state, and an output value of type `A`. State is used to represent computations that depend on a certain state, without having to thread it explicitly through all operations. 38 | */ 39 | struct State { 40 | let run: (S) -> (S, A) 41 | } 42 | /*: 43 | It's instance of the Monad type class (its implementation) is pretty straigthforward. Let's begin with `pure`: given any value of type `A`, we can always provide a `State`, that does not modify the passed state: 44 | */ 45 | extension State { 46 | static func pure(_ a: A) -> State { 47 | State { s in (s, a) } 48 | } 49 | } 50 | /*: 51 | As for `flatMap`, we mentioned that, from our intuition, we are sequencing two operations, where the second one depends on the result of the first. That means we should run the first State, obtain the modified state and the output, and feed it to the second: 52 | */ 53 | extension State { 54 | func flatMap(_ f: @escaping (A) -> State) -> State { 55 | State { s in 56 | let (newS, a) = self.run(s) 57 | return f(a).run(newS) 58 | } 59 | } 60 | } 61 | /*: 62 | ## Comonads 63 | 64 | Similarly, Comonads could be defined as *Comonoids in the Category of Endofunctors*, which is an equally useless definition in software development. Comonads the dual structure of Monads, obtained by reversing the arrows in Category Theory. 65 | 66 | As Monads, Comonads work at the Higher Kind level, and are only possible to be represented using the emulation provided by Bow. They require the implementation of the following requirements: 67 | 68 | - `extract`: `(F) -> A` 69 | - `coflatMap` or `extend`: `(F, (F) -> B) -> F` 70 | 71 | Sometimes, instead of `coflatMap`, you could implement: 72 | 73 | - `duplicate`: `(F) -> F>` 74 | 75 | `coflatMap` and `duplicate` can be implemented in terms of one another; thus, you need to implement `extract`, and `coflatMap` or `duplicate`, to have an implementation of a Comonad. In Bow, you will always have to implement `extract` and `coflatMap`. 76 | 77 | By now, you may have already noticed some symmetry between Monads and Comonads, but let's look at the types of the functions in order to build some sort of intuition behind them. 78 | 79 | `extract` is a function `(F) -> A`, which, if you pay attention, is just the opposite of `pure`. That is, given a value in the context of the Comonad, we are able to extract that value out of the context. This tells us the Comonad represents some kind of space, but it is focused on a specific point of such space, which we can always obtain. 80 | 81 | `coflatMap` is a function `(F, (F) -> B) -> F`. That is, we need to return an `F`, which as we have mentioned above, can be seen as a space of values of type `B`. The only way we can obtain values of type `B` is by the function `(F) -> B`, but an invocation of this function gives us a single point `B` in our space. This suggests we will need to invoke this function potentially multiple times to build the space of `F`, and each time, it consumes the context provided by `F`. Therefore, `coflatMap` lets us perform an operation `(F) -> B` that consumes the context of `F`, in all posible foci its space of values, to produce a new space of values. 82 | 83 | In summary, Comonads let us perform operations that are context-dependent, and extract their focused results. 84 | 85 | ### Example 86 | 87 | Store is also used extensively in Bow Arch. `Store` wraps two things: a value of type `S`, known as the `state`, and a function of type `(S) -> A`, known as `render`. 88 | */ 89 | struct Store { 90 | let state: S 91 | let render: (S) -> A 92 | } 93 | /*: 94 | From the intuition we built before, we said a Comonad represents a space of values. Such space is represented by the `render` function in the Store. It models all possible `A` values that could be potentially rendered by this Store. Also, we mentioned that Comonads are somehow focused on a specific point of such space; in Store, that focus is the `state`. 95 | 96 | How does its Comonoad instance look like? The `extract` function should be easy to implement: just apply the `render` function to the current `state`: 97 | */ 98 | extension Store { 99 | func extract() -> A { 100 | self.render(self.state) 101 | } 102 | } 103 | /*: 104 | The implementation for `coflatMap` may be a bit more cumbersome to understand. We need to provide a `Store`. The `state` property for such Store is the same `state` of the receiver Store, as we have no other way of getting such value. 105 | 106 | Regarding the `render` function, we need a function `(S) -> B`. The only thing we have to obtain a `B` is the provided function `(Store) -> B`. Therefore, we can construct a new Store with the `render` function of `Store`, and pass it to the provided function. 107 | */ 108 | extension Store { 109 | func coflatMap(_ f: @escaping (Store) -> B) -> Store { 110 | Store( 111 | state: self.state, 112 | render: { s in 113 | f(Store(state: s, render: self.render)) 114 | }) 115 | } 116 | } 117 | /*: 118 | As you can see, the function `(Store) -> B` is providing us a specific point of the new space in `Store`, and when we do a `coflatMap`, we are potentially exploring all possible contexts (with the new `render` function) to build the space of `Store`. 119 | 120 | Stores are focused on a specific state, but also provide methods to change that focus, to render a different point of the space of options they model. 121 | */ 122 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Background.playground/Pages/Overview.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Overview 5 | */ 6 | // nef:end 7 | /*: 8 | # Theoretical background 9 | 10 | This section describes the theoretical background behind Bow Arch. This library is based on concepts from a branch of Mathematics called Category Theory, which provides solid grounds to Functional Programming through a number of abstractions that obey a set of rules, and help us reason about our code. 11 | 12 | In particular, this library is based on the work done in PureScript by Phil Freeman - [Declarative UIs are the Future, and the Future is Comonadic!](https://functorial.com/the-future-is-comonadic/main.pdf) - and Arthur Xavier - [A Real World Application with a Comonadic User Interface](https://github.com/arthurxavierx/purescript-comonad-rss/blob/master/RealWorldAppComonadicUI.pdf). Some of the abstractions are also inspired by the [Haskell Comonads package](https://hackage.haskell.org/package/comonad), developed by Edward Kmett. The usage of optics to break down and compose components is inspired by the use of index and case paths in the [Swift Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) by Stephen Cellis and Brandon Williams. To all of them, we want to express our deepest gratitude. 13 | 14 | As you could guess, implementing these abstractions is not trivial, especially considering that Swift does not have Higher Kinded Types, nor existential quantifiers. These limitations have been partially overcome using emulation of HKTs in [Bow](https://bow-swift.io). The core abstractions - the Comonad package - are included in that library, as they can serve to other purposes; whereas their composition to create architectural components are included in Bow Arch. 15 | 16 | ## Why should I bother learning this? 17 | 18 | Understanding the theory behind the library is interesting and will give you a new perspective of its power, and the possibilities behind it. However, this is not necessary to be able to use the library to build complex applications. 19 | 20 | Software engineers have come up with many proposals to architect frontend applications in a functional manner. Examples of these are [Redux](https://redux.js.org/) or [Elm](https://guide.elm-lang.org/architecture/), or in the Swift community, [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture). 21 | 22 | Comonadic UIs are a generalization over these architectures, showing that most of the ideas that have appeared from intuition, actually have a solid background that support their validity. The aim of Bow Arch is to show these ideas in action, and how using different Comonads yields new ways of architecting applications, that are better suited for certain interaction patterns. 23 | 24 | As stated above, learning about Comonadic UIs is not necessary to be proficient at using Bow Arch or any other architectural library, but it will broaden your understanding of the architectural decisions that you are making, and you will be able to make more solid decisions when designing your applications. 25 | */ 26 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Background.playground/Pages/Pairings.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Pairings 5 | */ 6 | // nef:end 7 | /*: 8 | # Pairings 9 | 10 | Monads and Comonads are dual structures in Category Theory, and their connection can go even beyond that. Specific instances of Monads and Comonads can have a relationship between each other that will yield interesting results. That relationship is known as Pairing, and let us combine a Monad and its corresponding Comonad. 11 | 12 | We have mentioned that Comonads represent a space of options, from which one of these options is selected at a given moment. On the other hand, Monads model actions that produce a new context. A Pairing is a combination of a Monad and a Comonad in such a way that we can use values of the Monad to somehow navigate the space represented by the Comonad. This is done by implementing a function with the following signature: 13 | 14 | `pair: (F, G, (A, B) -> C) -> C` 15 | 16 | That is, given a Monad `F` producing values of type `A`, a Comonad `G` producing values of type `B`, and a way of combining `A` and `B` into `C`, the pair function can somehow extract the values of `A` and `B` from their corresponding Monad and Comonad, and combine them. Notice that this is not always possible; it implies that there must be some sort of relationship between `F` and `G` to be able to do this. 17 | 18 | ### Example 19 | 20 | By now this should look very abstract to you, so let's try to illustrate it with an example, using State and Store. 21 | 22 | If we look at State, it represents a function `(S) -> (S, A)`; therefore, we cannot extract an `A` from it by only using what we have inside it. However, if we pair it with a Store, we can use the `state` value stored in the Store to run the State function, obtain an `A` value, and render the new state to obtain a `B` value. 23 | 24 | With this, the implementation of `pair` for State and Store could look like: 25 | */ 26 | // nef:begin:hidden 27 | struct State { 28 | let run: (S) -> (S, A) 29 | } 30 | 31 | struct Store { 32 | let state: S 33 | let render: (S) -> A 34 | } 35 | // nef:end 36 | func pair( 37 | _ fa: State, 38 | _ gb: Store, 39 | _ f: @escaping (A, B) -> C 40 | ) -> C { 41 | let (newS, a) = fa.run(gb.state) 42 | let b = gb.render(newS) 43 | return f(a, b) 44 | } 45 | /*: 46 | As you can see, we have taken the current state of the Store, and used State to transition it to a new state, that is then rendered. Therefore, this shows we can use values of State to perform actions that navigate the space described by Store. 47 | 48 | ## Existing Pairings 49 | 50 | Besides the State-Store pairing, there are others that can be found. Examples of these are Writer-Traced or Reader-Env, and you can take a look at their implementation in the Bow repository. 51 | 52 | Is it always possible to write a Pairing for a Monad and a Comonad? It turns out that there is an important result that shows that for a given Comonad, there is always a Monad that pairs with it. However, the reverse is not necessarily true; that is, given a Monad, there may not be a Comonad that pairs with it. 53 | 54 | In the cases we have seen so far, for Comonads Store, Traced and Env, their corresponding pairing Monads exist and are well known. However, there are Comonads whose pairing Monads may not be so well known. How can we pair them? 55 | 56 | There is an interesting type known as `Co`, or `Transition`, which, given any Comonad, it will yield its corresponding pairing Monad. Even in the cases we already know, like Store, we could get a Co-Store that will behave exactly like State. 57 | */ 58 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Background.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Background.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Core concepts.playground/Pages/Component.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Dispatcher 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import BowArch 9 | // nef:end 10 | /*: 11 | # Component 12 | 13 | Components in Bow Arch put everything together. They manage to render a **State** in a **View**, which eventually produces **Inputs**, that are transformed by a **Dispatcher** into actions that modify the state, closing the loop. This is a unidirectional loop where each artifact has clear responsibilities and are build in a purely functional manner. That way, everything is easy to manage, can be tested in isolation, and is highly composable. 14 | 15 | Components are parameterized with four types: 16 | 17 | - Environment: the dependencies it needs to run state updates. 18 | - State: the state it needs to handle. 19 | - Input: the type of inputs that are produced within the component. 20 | - View: the SwiftUI view that renders its user interface. 21 | 22 | Therefore, to create a component, you need to provide: 23 | 24 | - An initial state. 25 | - A data structure with the dependencies it needs. 26 | - A dispatcher that transforms inputs into actions. 27 | - A view that renders the state. 28 | 29 | From this initial configuration, it will handle state changes and updates to the UI as a black box. Components conform to SwiftUI `View`, so they can be used as part of other views, or as the root view of a `UIHostingController`. 30 | */ 31 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Core concepts.playground/Pages/Dispatcher.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Dispatcher 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import BowArch 9 | import Bow 10 | import BowEffects 11 | import BowOptics 12 | // nef:end 13 | /*: 14 | # Dispatcher 15 | 16 | Inputs in the user interface need to be transformed into actions that trigger state changes. That is the responsibility of the Dispatcher: take inputs and transform them into actions that modify the state. 17 | 18 | Dispatchers have three parameters: 19 | 20 | - **Environment**: represents the dependencies we need to perform the actions for the received inputs. 21 | - **State**: represents the type of the state the dispatcher can modify when an input is received. 22 | - **Input**: represents the type of inputs the dispatcher is able to handle. 23 | 24 | Internally, a dispatcher is a function from the input type, to an array of side-effectful actions that depend on an environment and mutate a state; that is: 25 | 26 | `(I) -> [EnvIO>]` 27 | 28 | where: 29 | 30 | - `I` is the input type. 31 | - `EnvIO` is a type to model side-effectful operations that depend on an environment. 32 | - `E` is the environment type. 33 | - `State` is a monad that models state-based computations. 34 | - `S` is the state type. 35 | 36 | Each action in the array will trigger a UI refresh; this is because some inputs may trigger a UI update, then do a long-running task, and finally trigger a new UI update to show the final result. 37 | 38 | Nonetheless, we can create dispatchers for actions that involve single UI updates, or even ones that are side-effect free, with the following methods: 39 | 40 | - `pure`: lets us create side-effect free dispatchers that have no dependencies and state modification is pure. 41 | - `effectful`: lets us create single action dispatchers that may have dependencies and require side effects to modify the state. 42 | - `workflow`: lets us create side-effectful dispatchers that may have dependencies and perform multiple UI updates. 43 | 44 | ## Examples 45 | 46 | ### Pure Dispatcher 47 | 48 | Incrementing or decrementing the count of a stepper is a pure state modification, so we can create a dispatcher like: 49 | */ 50 | enum StepperInput { 51 | case tapDecrement 52 | case tapIncrement 53 | } 54 | 55 | typealias StepperDispatcher = StateDispatcher 56 | 57 | let stepperDispatcher = StepperDispatcher.pure { input in 58 | switch input { 59 | case .tapDecrement: 60 | return .modify { count in count - 1 }^ 61 | case .tapIncrement: 62 | return .modify { count in count + 1 }^ 63 | } 64 | } 65 | /*: 66 | ### Effectful Dispatcher 67 | 68 | Rolling a die is a side-effectful action, as it includes randomness. We can capture randomness in a dependency: 69 | */ 70 | protocol Randomness { 71 | func getInt(in range: ClosedRange) -> EnvIO 72 | } 73 | /*: 74 | Then, we can create our dispatcher as: 75 | */ 76 | enum DieInput { 77 | case roll 78 | } 79 | 80 | struct Die { 81 | let number: Int 82 | } 83 | 84 | typealias DieDispatcher = StateDispatcher 85 | 86 | let dieDispatcher = DieDispatcher.effectful { input in 87 | switch input { 88 | case .roll: 89 | return EnvIO.accessM { random in random.getInt(in: 1 ... 6) } 90 | .map { n in 91 | .set(Die(number: n))^ 92 | }^ 93 | } 94 | } 95 | /*: 96 | ### Workflow Dispatcher 97 | 98 | Finally, we can create a dispatcher that triggers multiple UI updates. For instance, we may show a loading indicator, fetch data from the network, and then show it in the UI. 99 | */ 100 | // Dependencies 101 | protocol Network { 102 | func load() -> EnvIO 103 | } 104 | 105 | // State 106 | enum ScreenState { 107 | case loading 108 | case loaded(Data) 109 | } 110 | 111 | // Input 112 | enum ScreenInput { 113 | case fetchData 114 | } 115 | 116 | // Dispatcher 117 | typealias ScreenDispatcher = StateDispatcher 118 | 119 | func showLoading() -> EnvIO> { 120 | EnvIO.pure(.set(.loading)^)^ 121 | } 122 | 123 | func showLoadedData() -> EnvIO> { 124 | let network = EnvIO.var() 125 | let data = EnvIO.var() 126 | 127 | return binding( 128 | continueOn(.global(qos: .background)), 129 | network <- .ask(), 130 | data <- network.get.load(), 131 | yield: .set(.loaded(data.get))^ 132 | )^ 133 | } 134 | 135 | let screenDispatcher = ScreenDispatcher.workflow { input in 136 | switch input { 137 | case .fetchData: 138 | return [ 139 | showLoading(), 140 | showLoadedData() 141 | ] 142 | } 143 | } 144 | /*: 145 | ## Combining Dispatchers 146 | 147 | As long as two dispatchers share the same type parameters, they can be combined, as they conform to `Semigroup`. 148 | 149 | If they don't have the same type parameters, they can be transformed using the `widen` method, which needs the following: 150 | 151 | - A function to extract the environment from a parent environment. 152 | - A lens to extract the state from a parent state. 153 | - A prism to extract the input from a parent input. 154 | 155 | For instance, consider the `screenDispatcher` above needs to be combined with a parent dispatcher that works on more general environment, state and input: 156 | */ 157 | // nef:begin:hidden 158 | protocol Database {} 159 | struct OtherState {} 160 | enum OtherInput {} 161 | // nef:end 162 | struct Dependencies { 163 | let network: Network 164 | let database: Database 165 | } 166 | 167 | struct ParentState { 168 | let screen: ScreenState 169 | let other: OtherState 170 | } 171 | 172 | enum ParentInput { 173 | case screen(ScreenInput) 174 | case other(OtherInput) 175 | } 176 | 177 | typealias ParentDispatcher = StateDispatcher 178 | /*: 179 | First, we need to create a lens and a prism to focus on the state and input of our child dispatcher: 180 | */ 181 | let screenLens = Lens( 182 | get: { parent in parent.screen }, 183 | set: { parent, newScreen in ParentState(screen: newScreen, other: parent.other) } 184 | ) 185 | 186 | extension ParentInput: AutoPrism {} 187 | let screenPrism = ParentInput.prism(for: ParentInput.screen) 188 | /*: 189 | Then, we can widen our `screenDispatcher` to have the same type parameters as the parent: 190 | */ 191 | let widenScreenDispatcher: ParentDispatcher = screenDispatcher.widen( 192 | transformEnvironment: { dependencies in dependencies.network }, 193 | transformState: screenLens, 194 | transformInput: screenPrism 195 | ) 196 | /*: 197 | And finally, we can combine both dispatchers: 198 | */ 199 | // nef:begin:hidden 200 | let parentDispatcher = ParentDispatcher.workflow { _ in [] } 201 | // nef:end 202 | let appDispatcher = parentDispatcher.combine(widenScreenDispatcher) 203 | /*: 204 | This lets us write very focused dispatchers that only receive what they need to perform their job, separate our concerns properly, and then have powerful ways to compose them into a single dispatcher that manages the logic of our application. 205 | */ 206 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Core concepts.playground/Pages/State and Input.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: State and Input 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import BowArch 9 | import BowOptics 10 | // nef:end 11 | /*: 12 | # State and Input 13 | 14 | Bow Arch encourages a strong modeling of the problem domain to capture the state and inputs of a component into immutable data structures, that are usually designed as Algebraic Data Types. 15 | 16 | How you model state and inputs is very dependent on your particular project. However, as a general guideline, state is usually modeled using a product type, and inputs are usually modeled as a sum type. 17 | 18 | Product types in Swift can be represented using structs, tuples or classes, and while being isomorphic, each one of them have different semantics and/or ergonomics: 19 | 20 | - Structs: provide value semantics, are final (no inheritance allowed), and mutability has to be marked explicitly. 21 | - Classes: provide reference semantics, can be extended, and mutability is not explicit. 22 | - Tuples: provide value semantics, but are not nominal types; therefore, we cannot add methods to them in an extension. 23 | 24 | Typically, modeling state with structs will be our preferred choice. 25 | 26 | As for Sum types, Swift provides enums to represent them. Swift enums can have associated values that will be the companion data we need to perform an action for the input they represent. 27 | 28 | ## Parent-child relationships 29 | 30 | Both state and input of a given component need to be captured in its parent state and input. That is, the parent state should have a field representing the child state; similarly, the parent input should have a case representing the child input. 31 | 32 | ## Accessing and modifying immutable data 33 | 34 | We have made a strong emphasis in modeling state and input as immutable data structures. How should you access and modify them? The answer is optics. 35 | 36 | [Optics](https://bow-swift.io/next/docs/optics/optics-overview/) are algebraic structures that let us work with immutable data structures in a functional way, and are highly composable. In particular, the optics that we will need are: 37 | 38 | - **Lenses**: they are optics to work with product types that let us view and modify a part of a data structure. 39 | - **Prisms**: they are optics to work with sum types that let us extract or embed a case of a data structure. 40 | 41 | You can use Bow Optics to [write your own lenses and prisms](https://bow-swift.io/next/docs/optics/writing-your-own-optics/), or [get them automatically generated](https://bow-swift.io/next/docs/optics/automatic-derivation/), to work with your data structures. 42 | 43 | ## Example 44 | 45 | Consider an app that renders a home screen with a user profile and a list of articles. Tapping on the user profile goes to a new screen to show a detail of the user profile, where we can perform editions. 46 | 47 | We can model state as: 48 | */ 49 | struct UserProfile { 50 | let name: String 51 | let picture: URL 52 | } 53 | 54 | struct Article { 55 | let title: String 56 | let content: String 57 | let publicationDate: Date 58 | let isFavorite: Bool 59 | } 60 | /*: 61 | State is then grouped into the parent state: 62 | */ 63 | struct HomeScreen { 64 | let profile: UserProfile 65 | let articles: [Article] 66 | } 67 | /*: 68 | Similarly, we can model inputs as: 69 | */ 70 | enum UserProfileInput { 71 | case changePicture(URL) 72 | case changeName(String) 73 | } 74 | 75 | enum ArticleInput { 76 | case markFavorite(Article) 77 | } 78 | /*: 79 | Inputs can also be grouped into a parent input: 80 | */ 81 | enum HomeScreenInput { 82 | case userProfile(UserProfileInput) 83 | case article(ArticleInput) 84 | } 85 | /*: 86 | We can write a lens to access the user profile from a home screen: 87 | */ 88 | let userProfileLens = Lens( 89 | get: { home in home.profile }, 90 | set: { home, newProfile in HomeScreen(profile: newProfile, articles: home.articles) } 91 | ) 92 | /*: 93 | Likewise, we can write a prism to access the user profile input from a home screen input: 94 | */ 95 | let userProfilePrism = Prism( 96 | extract: { homeInput in 97 | guard case let .userProfile(input) = homeInput else { 98 | return nil 99 | } 100 | return input 101 | }, 102 | embed: HomeScreenInput.userProfile 103 | ) 104 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Core concepts.playground/Pages/View.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: View 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import SwiftUI 9 | // nef:end 10 | /*: 11 | # View 12 | 13 | Views in BowArch are pure SwiftUI views that render the user interface as a function of immutable state. There is no restriction from Bow Arch on how you can build your View, and it is totally up to your particular app. 14 | 15 | Commonly, your view will receive two parameters: 16 | 17 | - A state, that you need to store as a field, and contains the information you need to render the view. 18 | - A function, that you can invoke whenever an input happens on the view. 19 | 20 | The state refers to the application state and it is immutable; that is, you must not mutate it, and do not need to store it with an `@State` property wrapper. Bow Arch will take care of its modification whenever an action triggers it. 21 | 22 | Your view may have other fields to hold its own internal state, that is not relevant to the application state. Those fields may be annotated with `@State` or `@Binding` if necessary. 23 | 24 | The view can also be decomposed into multiple, smaller views, in order to handle its complexity, and be able to reuse them as much as possible. 25 | 26 | ## Example 27 | 28 | As an example, we can build a stepper view. Given the following structures modeling state and input: 29 | */ 30 | struct StepperState { 31 | let count: Int 32 | } 33 | 34 | enum StepperInput { 35 | case tapDecrement 36 | case tapIncrement 37 | } 38 | /*: 39 | We can build the View as: 40 | */ 41 | struct StepperView: View { 42 | let state: StepperState 43 | let handle: (StepperInput) -> Void 44 | 45 | var body: some View { 46 | HStack { 47 | Button("-") { 48 | self.handle(.tapDecrement) 49 | } 50 | 51 | Text("\(state.count)") 52 | 53 | Button("+") { 54 | self.handle(.tapIncrement) 55 | } 56 | } 57 | } 58 | } 59 | /*: 60 | Inputs provided in this View will be received by a Dispatcher, which will trigger the corresponding actions to modify the state. This way, our view remains a pure function of state, and it is completely decoupled of the business logic. 61 | */ 62 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Core concepts.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Core concepts.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Documentation.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Documentation.xcodeproj/xcshareddata/xcschemes/Documentation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Documentation.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Documentation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Documentation/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1.0 21 | NSHumanReadableCopyright 22 | Copyright © 2019. The nef authors. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Legal.playground/Pages/Credits.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Credits 5 | */ 6 | // nef:end 7 | /*: 8 | # Credits 9 | 10 | 2020 - © The Bow Authors 11 | 12 | For a comprehensive list of contributors, [visit the repository](https://github.com/bow-swift/bow-arch/graphs/contributors) on GitHub. 13 | 14 | We want to thank Arthur Xavier, Phil Freeman, and Edward Kmett, for their previous work on Comonads and Comonadic UIs in the PureScript and Haskell languages; and Stephen Cellis and Brandon Williams, for their work on the Swift Composable Architecture. Their work has inspired the creation of this library. 15 | 16 | Bow Arch is supported by [47 Degrees](https://www.47deg.com). 17 | */ 18 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Legal.playground/Pages/License.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: License 5 | */ 6 | // nef:end 7 | /*: 8 | # License 9 | 10 | Copyright 2020 The Bow Authors 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | 24 | ------------------------------------------------------------------------ 25 | 26 | Apache License 27 | Version 2.0, January 2004 28 | http://www.apache.org/licenses/ 29 | 30 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 31 | 32 | 1. Definitions. 33 | 34 | "License" shall mean the terms and conditions for use, reproduction, 35 | and distribution as defined by Sections 1 through 9 of this document. 36 | 37 | "Licensor" shall mean the copyright owner or entity authorized by 38 | the copyright owner that is granting the License. 39 | 40 | "Legal Entity" shall mean the union of the acting entity and all 41 | other entities that control, are controlled by, or are under common 42 | control with that entity. For the purposes of this definition, 43 | "control" means (i) the power, direct or indirect, to cause the 44 | direction or management of such entity, whether by contract or 45 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 46 | outstanding shares, or (iii) beneficial ownership of such entity. 47 | 48 | "You" (or "Your") shall mean an individual or Legal Entity 49 | exercising permissions granted by this License. 50 | 51 | "Source" form shall mean the preferred form for making modifications, 52 | including but not limited to software source code, documentation 53 | source, and configuration files. 54 | 55 | "Object" form shall mean any form resulting from mechanical 56 | transformation or translation of a Source form, including but 57 | not limited to compiled object code, generated documentation, 58 | and conversions to other media types. 59 | 60 | "Work" shall mean the work of authorship, whether in Source or 61 | Object form, made available under the License, as indicated by a 62 | copyright notice that is included in or attached to the work 63 | (an example is provided in the Appendix below). 64 | 65 | "Derivative Works" shall mean any work, whether in Source or Object 66 | form, that is based on (or derived from) the Work and for which the 67 | editorial revisions, annotations, elaborations, or other modifications 68 | represent, as a whole, an original work of authorship. For the purposes 69 | of this License, Derivative Works shall not include works that remain 70 | separable from, or merely link (or bind by name) to the interfaces of, 71 | the Work and Derivative Works thereof. 72 | 73 | "Contribution" shall mean any work of authorship, including 74 | the original version of the Work and any modifications or additions 75 | to that Work or Derivative Works thereof, that is intentionally 76 | submitted to Licensor for inclusion in the Work by the copyright owner 77 | or by an individual or Legal Entity authorized to submit on behalf of 78 | the copyright owner. For the purposes of this definition, "submitted" 79 | means any form of electronic, verbal, or written communication sent 80 | to the Licensor or its representatives, including but not limited to 81 | communication on electronic mailing lists, source code control systems, 82 | and issue tracking systems that are managed by, or on behalf of, the 83 | Licensor for the purpose of discussing and improving the Work, but 84 | excluding communication that is conspicuously marked or otherwise 85 | designated in writing by the copyright owner as "Not a Contribution." 86 | 87 | "Contributor" shall mean Licensor and any individual or Legal Entity 88 | on behalf of whom a Contribution has been received by Licensor and 89 | subsequently incorporated within the Work. 90 | 91 | 2. Grant of Copyright License. Subject to the terms and conditions of 92 | this License, each Contributor hereby grants to You a perpetual, 93 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 94 | copyright license to reproduce, prepare Derivative Works of, 95 | publicly display, publicly perform, sublicense, and distribute the 96 | Work and such Derivative Works in Source or Object form. 97 | 98 | 3. Grant of Patent License. Subject to the terms and conditions of 99 | this License, each Contributor hereby grants to You a perpetual, 100 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 101 | (except as stated in this section) patent license to make, have made, 102 | use, offer to sell, sell, import, and otherwise transfer the Work, 103 | where such license applies only to those patent claims licensable 104 | by such Contributor that are necessarily infringed by their 105 | Contribution(s) alone or by combination of their Contribution(s) 106 | with the Work to which such Contribution(s) was submitted. If You 107 | institute patent litigation against any entity (including a 108 | cross-claim or counterclaim in a lawsuit) alleging that the Work 109 | or a Contribution incorporated within the Work constitutes direct 110 | or contributory patent infringement, then any patent licenses 111 | granted to You under this License for that Work shall terminate 112 | as of the date such litigation is filed. 113 | 114 | 4. Redistribution. You may reproduce and distribute copies of the 115 | Work or Derivative Works thereof in any medium, with or without 116 | modifications, and in Source or Object form, provided that You 117 | meet the following conditions: 118 | 119 | (a) You must give any other recipients of the Work or 120 | Derivative Works a copy of this License; and 121 | 122 | (b) You must cause any modified files to carry prominent notices 123 | stating that You changed the files; and 124 | 125 | (c) You must retain, in the Source form of any Derivative Works 126 | that You distribute, all copyright, patent, trademark, and 127 | attribution notices from the Source form of the Work, 128 | excluding those notices that do not pertain to any part of 129 | the Derivative Works; and 130 | 131 | (d) If the Work includes a "NOTICE" text file as part of its 132 | distribution, then any Derivative Works that You distribute must 133 | include a readable copy of the attribution notices contained 134 | within such NOTICE file, excluding those notices that do not 135 | pertain to any part of the Derivative Works, in at least one 136 | of the following places: within a NOTICE text file distributed 137 | as part of the Derivative Works; within the Source form or 138 | documentation, if provided along with the Derivative Works; or, 139 | within a display generated by the Derivative Works, if and 140 | wherever such third-party notices normally appear. The contents 141 | of the NOTICE file are for informational purposes only and 142 | do not modify the License. You may add Your own attribution 143 | notices within Derivative Works that You distribute, alongside 144 | or as an addendum to the NOTICE text from the Work, provided 145 | that such additional attribution notices cannot be construed 146 | as modifying the License. 147 | 148 | You may add Your own copyright statement to Your modifications and 149 | may provide additional or different license terms and conditions 150 | for use, reproduction, or distribution of Your modifications, or 151 | for any such Derivative Works as a whole, provided Your use, 152 | reproduction, and distribution of the Work otherwise complies with 153 | the conditions stated in this License. 154 | 155 | 5. Submission of Contributions. Unless You explicitly state otherwise, 156 | any Contribution intentionally submitted for inclusion in the Work 157 | by You to the Licensor shall be under the terms and conditions of 158 | this License, without any additional terms or conditions. 159 | Notwithstanding the above, nothing herein shall supersede or modify 160 | the terms of any separate license agreement you may have executed 161 | with Licensor regarding such Contributions. 162 | 163 | 6. Trademarks. This License does not grant permission to use the trade 164 | names, trademarks, service marks, or product names of the Licensor, 165 | except as required for reasonable and customary use in describing the 166 | origin of the Work and reproducing the content of the NOTICE file. 167 | 168 | 7. Disclaimer of Warranty. Unless required by applicable law or 169 | agreed to in writing, Licensor provides the Work (and each 170 | Contributor provides its Contributions) on an "AS IS" BASIS, 171 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 172 | implied, including, without limitation, any warranties or conditions 173 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 174 | PARTICULAR PURPOSE. You are solely responsible for determining the 175 | appropriateness of using or redistributing the Work and assume any 176 | risks associated with Your exercise of permissions under this License. 177 | 178 | 8. Limitation of Liability. In no event and under no legal theory, 179 | whether in tort (including negligence), contract, or otherwise, 180 | unless required by applicable law (such as deliberate and grossly 181 | negligent acts) or agreed to in writing, shall any Contributor be 182 | liable to You for damages, including any direct, indirect, special, 183 | incidental, or consequential damages of any character arising as a 184 | result of this License or out of the use or inability to use the 185 | Work (including but not limited to damages for loss of goodwill, 186 | work stoppage, computer failure or malfunction, or any and all 187 | other commercial damages or losses), even if such Contributor 188 | has been advised of the possibility of such damages. 189 | 190 | 9. Accepting Warranty or Additional Liability. While redistributing 191 | the Work or Derivative Works thereof, You may choose to offer, 192 | and charge a fee for, acceptance of support, warranty, indemnity, 193 | or other liability obligations and/or rights consistent with this 194 | License. However, in accepting such obligations, You may act only 195 | on Your own behalf and on Your sole responsibility, not on behalf 196 | of any other Contributor, and only if You agree to indemnify, 197 | defend, and hold each Contributor harmless for any liability 198 | incurred by, or claims asserted against, such Contributor by reason 199 | of your accepting any such warranty or additional liability. 200 | */ 201 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Legal.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Legal.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Patterns.playground/Pages/Creating a single component.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Creating a single component 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import Bow 9 | import BowEffects 10 | import BowArch 11 | // nef:end 12 | /*: 13 | # Creating a single component 14 | 15 | Bow Arch lets you architect your application in terms of components that can be reused across applications. Let's go over what you need to create in order to build a stepper component. 16 | 17 | ## 📋 State 18 | 19 | A component should have a state that is rendered in its view. State is usually modeled using an immutable data structure, typically a `struct`. 20 | 21 | For our stepper component, we can model our state as: 22 | */ 23 | struct StepperState { 24 | let count: Int 25 | } 26 | /*: 27 | ## 📲 Input 28 | 29 | Next step is modeling inputs that a component can handle. Inputs are usually described using cases of an `enum`. 30 | 31 | In our ongoing example, the component can receive two inputs, corresponding to tapping on the decrement or increment buttons. These can be modeled as: 32 | */ 33 | enum StepperInput { 34 | case tapDecrement 35 | case tapIncrement 36 | } 37 | /*: 38 | ## 🎨 View 39 | 40 | With state and input defined, we can render a view using SwiftUI. SwiftUI is a declarative framework to describe user interfaces in Swift, with multiple bindings for the different operating systems in the Apple Platforms. 41 | 42 | We can describe the view as a function of its state, and use a function to receive inputs: 43 | */ 44 | import SwiftUI 45 | 46 | struct StepperView: View { 47 | let state: StepperState 48 | let handle: (StepperInput) -> Void 49 | 50 | var body: some View { 51 | HStack { 52 | Button("-") { 53 | self.handle(.tapDecrement) 54 | } 55 | 56 | Text("\(state.count)") 57 | 58 | Button("+") { 59 | self.handle(.tapIncrement) 60 | } 61 | } 62 | } 63 | } 64 | /*: 65 | ## 🔨 Dispatcher 66 | 67 | Inputs new to be transformed into actions that modify the state. This is done at the Dispatcher. Dispatchers are pure functions that receive inputs and produce actions: 68 | */ 69 | typealias StepperDispatcher = StateDispatcher 70 | 71 | let stepperDispatcher = StepperDispatcher.pure { input in 72 | switch input { 73 | case .tapDecrement: 74 | return .modify { state in 75 | StepperState(count: state.count - 1) 76 | }^ 77 | 78 | case .tapIncrement: 79 | return .modify { state in 80 | StepperState(count: state.count + 1) 81 | }^ 82 | } 83 | } 84 | /*: 85 | ## 🧩 Component 86 | 87 | Finally, we can put everything together as a component: 88 | */ 89 | typealias StepperComponent = StoreComponent 90 | 91 | let stepperComponent = StepperComponent( 92 | initialState: StepperState(count: 0), 93 | dispatcher: stepperDispatcher, 94 | render: StepperView.init) 95 | /*: 96 | Components already conform to SwiftUI `View`, so they can be used as part of other views, or assigned as the root view of a `UIHostingController`. 97 | */ 98 | let controller = UIHostingController(rootView: stepperComponent) 99 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Patterns.playground/Pages/Creating nested components.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Creating nested components 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import SwiftUI 9 | import Bow 10 | import BowEffects 11 | import BowOptics 12 | import BowArch 13 | // nef:end 14 | /*: 15 | # Creating nested components 16 | 17 | Usually, you will need to create new components in terms of others with lower granularity. This page covers the most important aspects you need to consider when composing different components. 18 | 19 | ## Creating a parent view 20 | 21 | As components conform to `View`, nothing stops you from adding them to your SwiftUI views. Therefore, assuming you already have a `ChildComponent`, you can easily add it to your view hierarchy: 22 | */ 23 | // nef:begin:hidden 24 | struct ChildDependencies {} 25 | struct ChildState {} 26 | enum ChildInput {} 27 | struct ChildView: View { 28 | var body: some View { 29 | EmptyView() 30 | } 31 | } 32 | typealias ChildComponent = StoreComponent 33 | 34 | struct ParentState { 35 | let childState: ChildState 36 | 37 | static var childLens: Lens = Lens( 38 | get: \.childState, set: { ParentState(childState: $1) }) 39 | } 40 | 41 | enum ParentInput: AutoPrism { 42 | case childInput(ChildInput) 43 | } 44 | 45 | struct ParentDependencies { 46 | let childDependencies: ChildDependencies 47 | } 48 | 49 | class Snippet1 { 50 | // nef:end 51 | struct ParentView: View { 52 | let state: ParentState 53 | let child: ChildComponent 54 | 55 | var body: some View { 56 | VStack { 57 | // ... Some views ... 58 | 59 | child 60 | 61 | // ... Some views ... 62 | } 63 | } 64 | } 65 | // nef:begin:hidden 66 | } 67 | // nef:end 68 | /*: 69 | However, this forces to build the `ChildComponent` if, for instance, we would like to create a preview of `ParentView`. An alternative way of doing this is by parameterizing the `ParentView`: 70 | */ 71 | // nef:begin:hidden 72 | class Snippet2 { 73 | // nef:end 74 | struct ParentView: View { 75 | let state: ParentState 76 | let child: Child 77 | 78 | var body: some View { 79 | VStack { 80 | // ... Some views ... 81 | 82 | child 83 | 84 | // ... Some views ... 85 | } 86 | } 87 | } 88 | // nef:begin:hidden 89 | } 90 | // nef:end 91 | /*: 92 | With this small change, we can now pass the ChildComponent, or any other stub view that we want in order to render the preview. 93 | 94 | One additional step we can take is to pass a function that, given the child state, builds the corresponding component. This is particularly useful when we are dealing with collections of items. 95 | */ 96 | struct ParentView: View { 97 | let state: ParentState 98 | let child: (ChildState) -> Child 99 | let handle: (ParentInput) -> Void 100 | 101 | var body: some View { 102 | VStack { 103 | // ... Some views ... 104 | 105 | child(self.state.childState) 106 | 107 | // ... Some views ... 108 | } 109 | } 110 | } 111 | /*: 112 | ## Creating a global dispatcher 113 | 114 | Next, both child and parent will have their own Dispatchers to interpret view inputs into state mutations. Those Dispatchers need to be combined into a single one. However, types of both dispatchers do not match: 115 | */ 116 | typealias ChildDispatcher = StateDispatcher 117 | 118 | typealias ParentDispatcher = StateDispatcher 119 | /*: 120 | In order to combine them, first we need to match their type signatures. As the `ChildDependencies`, `ChildState` and `ChildInput` are embedded into `ParentDependencies`, `ParentState` and `ParentInput` respectively, we can `widen` the `ChildDispatcher` to embed each parameter into its corresponding slot in the parent: 121 | */ 122 | // nef:begin:hidden 123 | let childDispatcher = ChildDispatcher.workflow { _ in [] } 124 | let parentDispatcher = ParentDispatcher.workflow { _ in [] } 125 | // nef:end 126 | 127 | let widenChildDispatcher: ParentDispatcher = 128 | childDispatcher.widen( 129 | transformEnvironment: \.childDependencies, 130 | transformState: ParentState.childLens, 131 | transformInput: ParentInput.prism(for: ParentInput.childInput)) 132 | 133 | let combinedDispatcher = parentDispatcher.combine(widenChildDispatcher) 134 | /*: 135 | ## Assembling the parent component 136 | 137 | Finally, when you create the parent component, you will need to forward the inputs that happen on the child component to the parent component, as some of these inputs may be relevant to other components that are upstream in the view hierarchy. 138 | */ 139 | // nef:begin:hidden 140 | typealias ParentComponent = StoreComponent> 141 | 142 | func childComponent(_ state: ChildState) -> ChildComponent { 143 | fatalError() 144 | } 145 | // nef:end 146 | 147 | func parentComponent( 148 | initialState: ParentState, 149 | dependencies: ParentDependencies 150 | ) -> ParentComponent { 151 | ParentComponent( 152 | initialState: initialState, 153 | environment: dependencies, 154 | dispatcher: combinedDispatcher, 155 | render: { state, handle in 156 | ParentView( 157 | state: state, 158 | child: { childState in 159 | childComponent(childState) 160 | .using(handle, 161 | transformInput: ParentInput.prism(for: ParentInput.childInput)) 162 | }, 163 | handle: handle) 164 | } 165 | ) 166 | } 167 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Patterns.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Patterns.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Podfile: -------------------------------------------------------------------------------- 1 | target 'Documentation' do 2 | platform :ios, 13.0 3 | use_frameworks! 4 | 5 | pod "BowArch", :path => '../../..' 6 | end 7 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Quick start.playground/Pages/Getting started.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Getting started 5 | */ 6 | // nef:end 7 | // nef:begin:hidden 8 | import Bow 9 | import BowArch 10 | // nef:end 11 | /*: 12 | # Getting started 13 | 14 | This is an overview of the steps you need to follow to create your first component in Bow Arch. 15 | 16 | ## 📦 Adding Bow Arch as a dependency 17 | 18 | Bow Arch is available through Swift Package Manager, integrated in Xcode. You only need to use the repository URL on GitHub and the version or branch you would like to use. Alternatively, you can describe this dependency in your `Package.swift` file by adding the line: 19 | 20 | ```swift 21 | .package(url: "https://github.com/bow-swift/bow-arch.git", from: "{version}") 22 | ``` 23 | 24 | ## 📋 State 25 | 26 | Bow Arch lets you architect your application in terms of components that can be reused across applications. Let's go over what you need to create in order to build a stepper component. 27 | 28 | A component should have a state that is rendered in its view. State is usually modeled using an immutable data structure, typically a `struct`. 29 | 30 | For our stepper component, we can model our state as: 31 | */ 32 | struct StepperState { 33 | let count: Int 34 | } 35 | /*: 36 | ## 📲 Input 37 | 38 | Next step is modeling inputs that a component can handle. Inputs are usually described using cases of an `enum`. 39 | 40 | In our ongoing example, the component can receive two inputs, corresponding to tapping on the decrement or increment buttons. These can be modeled as: 41 | */ 42 | enum StepperInput { 43 | case tapDecrement 44 | case tapIncrement 45 | } 46 | /*: 47 | ## 🎨 View 48 | 49 | With state and input defined, we can render a view using SwiftUI. SwiftUI is a declarative framework to describe user interfaces in Swift, with multiple bindings for the different operating systems in the Apple Platforms. 50 | 51 | We can describe the view as a function of its state, and use a function to receive inputs: 52 | */ 53 | import SwiftUI 54 | 55 | struct StepperView: View { 56 | let state: StepperState 57 | let handle: (StepperInput) -> Void 58 | 59 | var body: some View { 60 | HStack { 61 | Button("-") { 62 | self.handle(.tapDecrement) 63 | } 64 | 65 | Text("\(state.count)") 66 | 67 | Button("+") { 68 | self.handle(.tapIncrement) 69 | } 70 | } 71 | } 72 | } 73 | /*: 74 | ## 🔨 Dispatcher 75 | 76 | Inputs new to be transformed into actions that modify the state. This is done at the Dispatcher. Dispatchers are pure functions that receive inputs and produce actions: 77 | */ 78 | typealias StepperDispatcher = StateDispatcher 79 | 80 | let stepperDispatcher = StepperDispatcher.pure { input in 81 | switch input { 82 | case .tapDecrement: 83 | return .modify { state in 84 | StepperState(count: state.count - 1) 85 | }^ 86 | 87 | case .tapIncrement: 88 | return .modify { state in 89 | StepperState(count: state.count + 1) 90 | }^ 91 | } 92 | } 93 | /*: 94 | ## 🧩 Component 95 | 96 | Finally, we can put everything together as a component: 97 | */ 98 | typealias StepperComponent = StoreComponent 99 | 100 | let stepperComponent = StepperComponent( 101 | initialState: StepperState(count: 0), 102 | dispatcher: stepperDispatcher, 103 | render: StepperView.init) 104 | /*: 105 | Components already conform to SwiftUI `View`, so they can be used as part of other views, or assigned as the root view of a `UIHostingController`. 106 | 107 | This is a quick walkthrough of the main concepts used in the library. There is more to each of them; refer to each specific documentation page to learn more about them. 108 | */ 109 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Quick start.playground/Pages/Resources.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // nef:begin:header 2 | /* 3 | layout: docs 4 | title: Resources 5 | */ 6 | // nef:end 7 | /*: 8 | # Resources 9 | 10 | In this section you can find a list of projects, libraries, presentations or blog posts that use Bow Arch: 11 | 12 | ## GitHub projects 13 | 14 | - [nef Playgrounds for iPad](https://github.com/bow-swift/nef-editor-client): An iPad application to create Swift Playgrounds with 3rd party dependencies. 15 | 16 | ## Libraries 17 | 18 | Be the first one showing here! 19 | 20 | ## Presentations 21 | 22 | Be the first one showing here! 23 | 24 | ## Blog posts 25 | 26 | - [Introducing Bow Arch 0.1.0](https://www.47deg.com/blog/bow-arch-0-1-0-release/) 27 | 28 | */ 29 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Quick start.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/Quick start.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Documentation.app/Contents/MacOS/launcher: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | workspace="Documentation.xcworkspace" 4 | workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) 5 | 6 | open "`pwd`/$workspacePath/$workspace" 7 | -------------------------------------------------------------------------------- /Documentation.app/Contents/Resources/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/bow-arch/b4a96c3bfad32c5dc7ccee1fae490caa453030e9/Documentation.app/Contents/Resources/AppIcon.icns -------------------------------------------------------------------------------- /Documentation.app/Contents/Resources/Assets.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/bow-arch/b4a96c3bfad32c5dc7ccee1fae490caa453030e9/Documentation.app/Contents/Resources/Assets.car -------------------------------------------------------------------------------- /Documentation.app/Jekyll/Home.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Home 4 | permalink: /docs/ 5 | --- 6 | 7 | # Bow Arch 8 | 9 | Bow Arch is a highly opinionated library to architect SwiftUI applications in a purely functional manner. It has a solid theoretical background behind it, whilst users of the library do not need to be proficient in such details to use it to its full power. 10 | 11 | Bow Arch is based on the following principles: 12 | 13 | ## 🎨 View as a function of state 14 | 15 | Using SwiftUI, we can create user interfaces in a declarative manner, that are a representation of a given state. The library goes even further and promotes the creation of views that are based on immutable state. 16 | 17 | ## 🚧 Clear separation of concerns 18 | 19 | The core concepts in the library are state, input, dispatcher, view, and component. Each one of them deals with a specific concern, and lets us separate how our code deals with different aspects of application development. 20 | 21 | ## 📦 Modularity 22 | 23 | The library promotes the creation of components that can be easily reused across the application, or even in other applications. These components are highly composable and let us manage the complexity of large applications. 24 | 25 | ## ✅ Testability 26 | 27 | Functional code is intrinsically testable; therefore, software created with Bow Arch is easy to test. The library also provides utilities that you can leverage to write powerful and expressive tests. 28 | 29 | ## 🧩 Highly polymorphic 30 | 31 | The library is based on abstract, parameterized artifacts. This makes this library not only a library to architect your application, but a library to create different architectures by replacing each of these parameters. Nevertheless, specific bindings are provided in the library, so that users do not have to deal with these details. 32 | 33 | ## 🧮 Mathematical background 34 | 35 | Bow Arch is based on concepts from Category Theory, which brings soundness to the reasoning we can do about our code. Nonetheless, the API of the library hides the complexity of these concepts and users do not need to be experts in this topic to use the library in their applications. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Bow Authors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | ------------------------------------------------------------------------ 16 | 17 | Apache License 18 | Version 2.0, January 2004 19 | http://www.apache.org/licenses/ 20 | 21 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 22 | 23 | 1. Definitions. 24 | 25 | "License" shall mean the terms and conditions for use, reproduction, 26 | and distribution as defined by Sections 1 through 9 of this document. 27 | 28 | "Licensor" shall mean the copyright owner or entity authorized by 29 | the copyright owner that is granting the License. 30 | 31 | "Legal Entity" shall mean the union of the acting entity and all 32 | other entities that control, are controlled by, or are under common 33 | control with that entity. For the purposes of this definition, 34 | "control" means (i) the power, direct or indirect, to cause the 35 | direction or management of such entity, whether by contract or 36 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 37 | outstanding shares, or (iii) beneficial ownership of such entity. 38 | 39 | "You" (or "Your") shall mean an individual or Legal Entity 40 | exercising permissions granted by this License. 41 | 42 | "Source" form shall mean the preferred form for making modifications, 43 | including but not limited to software source code, documentation 44 | source, and configuration files. 45 | 46 | "Object" form shall mean any form resulting from mechanical 47 | transformation or translation of a Source form, including but 48 | not limited to compiled object code, generated documentation, 49 | and conversions to other media types. 50 | 51 | "Work" shall mean the work of authorship, whether in Source or 52 | Object form, made available under the License, as indicated by a 53 | copyright notice that is included in or attached to the work 54 | (an example is provided in the Appendix below). 55 | 56 | "Derivative Works" shall mean any work, whether in Source or Object 57 | form, that is based on (or derived from) the Work and for which the 58 | editorial revisions, annotations, elaborations, or other modifications 59 | represent, as a whole, an original work of authorship. For the purposes 60 | of this License, Derivative Works shall not include works that remain 61 | separable from, or merely link (or bind by name) to the interfaces of, 62 | the Work and Derivative Works thereof. 63 | 64 | "Contribution" shall mean any work of authorship, including 65 | the original version of the Work and any modifications or additions 66 | to that Work or Derivative Works thereof, that is intentionally 67 | submitted to Licensor for inclusion in the Work by the copyright owner 68 | or by an individual or Legal Entity authorized to submit on behalf of 69 | the copyright owner. For the purposes of this definition, "submitted" 70 | means any form of electronic, verbal, or written communication sent 71 | to the Licensor or its representatives, including but not limited to 72 | communication on electronic mailing lists, source code control systems, 73 | and issue tracking systems that are managed by, or on behalf of, the 74 | Licensor for the purpose of discussing and improving the Work, but 75 | excluding communication that is conspicuously marked or otherwise 76 | designated in writing by the copyright owner as "Not a Contribution." 77 | 78 | "Contributor" shall mean Licensor and any individual or Legal Entity 79 | on behalf of whom a Contribution has been received by Licensor and 80 | subsequently incorporated within the Work. 81 | 82 | 2. Grant of Copyright License. Subject to the terms and conditions of 83 | this License, each Contributor hereby grants to You a perpetual, 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 85 | copyright license to reproduce, prepare Derivative Works of, 86 | publicly display, publicly perform, sublicense, and distribute the 87 | Work and such Derivative Works in Source or Object form. 88 | 89 | 3. Grant of Patent License. Subject to the terms and conditions of 90 | this License, each Contributor hereby grants to You a perpetual, 91 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 92 | (except as stated in this section) patent license to make, have made, 93 | use, offer to sell, sell, import, and otherwise transfer the Work, 94 | where such license applies only to those patent claims licensable 95 | by such Contributor that are necessarily infringed by their 96 | Contribution(s) alone or by combination of their Contribution(s) 97 | with the Work to which such Contribution(s) was submitted. If You 98 | institute patent litigation against any entity (including a 99 | cross-claim or counterclaim in a lawsuit) alleging that the Work 100 | or a Contribution incorporated within the Work constitutes direct 101 | or contributory patent infringement, then any patent licenses 102 | granted to You under this License for that Work shall terminate 103 | as of the date such litigation is filed. 104 | 105 | 4. Redistribution. You may reproduce and distribute copies of the 106 | Work or Derivative Works thereof in any medium, with or without 107 | modifications, and in Source or Object form, provided that You 108 | meet the following conditions: 109 | 110 | (a) You must give any other recipients of the Work or 111 | Derivative Works a copy of this License; and 112 | 113 | (b) You must cause any modified files to carry prominent notices 114 | stating that You changed the files; and 115 | 116 | (c) You must retain, in the Source form of any Derivative Works 117 | that You distribute, all copyright, patent, trademark, and 118 | attribution notices from the Source form of the Work, 119 | excluding those notices that do not pertain to any part of 120 | the Derivative Works; and 121 | 122 | (d) If the Work includes a "NOTICE" text file as part of its 123 | distribution, then any Derivative Works that You distribute must 124 | include a readable copy of the attribution notices contained 125 | within such NOTICE file, excluding those notices that do not 126 | pertain to any part of the Derivative Works, in at least one 127 | of the following places: within a NOTICE text file distributed 128 | as part of the Derivative Works; within the Source form or 129 | documentation, if provided along with the Derivative Works; or, 130 | within a display generated by the Derivative Works, if and 131 | wherever such third-party notices normally appear. The contents 132 | of the NOTICE file are for informational purposes only and 133 | do not modify the License. You may add Your own attribution 134 | notices within Derivative Works that You distribute, alongside 135 | or as an addendum to the NOTICE text from the Work, provided 136 | that such additional attribution notices cannot be construed 137 | as modifying the License. 138 | 139 | You may add Your own copyright statement to Your modifications and 140 | may provide additional or different license terms and conditions 141 | for use, reproduction, or distribution of Your modifications, or 142 | for any such Derivative Works as a whole, provided Your use, 143 | reproduction, and distribution of the Work otherwise complies with 144 | the conditions stated in this License. 145 | 146 | 5. Submission of Contributions. Unless You explicitly state otherwise, 147 | any Contribution intentionally submitted for inclusion in the Work 148 | by You to the Licensor shall be under the terms and conditions of 149 | this License, without any additional terms or conditions. 150 | Notwithstanding the above, nothing herein shall supersede or modify 151 | the terms of any separate license agreement you may have executed 152 | with Licensor regarding such Contributions. 153 | 154 | 6. Trademarks. This License does not grant permission to use the trade 155 | names, trademarks, service marks, or product names of the Licensor, 156 | except as required for reasonable and customary use in describing the 157 | origin of the Work and reproducing the content of the NOTICE file. 158 | 159 | 7. Disclaimer of Warranty. Unless required by applicable law or 160 | agreed to in writing, Licensor provides the Work (and each 161 | Contributor provides its Contributions) on an "AS IS" BASIS, 162 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 163 | implied, including, without limitation, any warranties or conditions 164 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 165 | PARTICULAR PURPOSE. You are solely responsible for determining the 166 | appropriateness of using or redistributing the Work and assume any 167 | risks associated with Your exercise of permissions under this License. 168 | 169 | 8. Limitation of Liability. In no event and under no legal theory, 170 | whether in tort (including negligence), contract, or otherwise, 171 | unless required by applicable law (such as deliberate and grossly 172 | negligent acts) or agreed to in writing, shall any Contributor be 173 | liable to You for damages, including any direct, indirect, special, 174 | incidental, or consequential damages of any character arising as a 175 | result of this License or out of the use or inability to use the 176 | Work (including but not limited to damages for loss of goodwill, 177 | work stoppage, computer failure or malfunction, or any and all 178 | other commercial damages or losses), even if such Contributor 179 | has been advised of the possibility of such damages. 180 | 181 | 9. Accepting Warranty or Additional Liability. While redistributing 182 | the Work or Derivative Works thereof, You may choose to offer, 183 | and charge a fee for, acceptance of support, warranty, indemnity, 184 | or other liability obligations and/or rights consistent with this 185 | License. However, in accepting such obligations, You may act only 186 | on Your own behalf and on Your sole responsibility, not on behalf 187 | of any other Contributor, and only if You agree to indemnify, 188 | defend, and hold each Contributor harmless for any liability 189 | incurred by, or claims asserted against, such Contributor by reason 190 | of your accepting any such warranty or additional liability. 191 | 192 | END OF TERMS AND CONDITIONS 193 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Bow", 6 | "repositoryURL": "https://github.com/bow-swift/bow.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "17ff76f1e0427a67e221c0a20b96324d256c340f", 10 | "version": "0.8.0" 11 | } 12 | }, 13 | { 14 | "package": "RxSwift", 15 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "002d325b0bdee94e7882e1114af5ff4fe1e96afa", 19 | "version": "5.1.1" 20 | } 21 | }, 22 | { 23 | "package": "SwiftCheck", 24 | "repositoryURL": "https://github.com/bow-swift/SwiftCheck.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "748359f9a95edf94d0c4664102f104f56b1ff1fb", 28 | "version": "0.12.1" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "BowArch", 7 | platforms: [ 8 | .macOS(.v10_15), .iOS(.v13) 9 | ], 10 | products: [ 11 | .library(name: "BowArch", targets: ["BowArch"]) 12 | ], 13 | 14 | dependencies: [ 15 | .package(name: "Bow", url: "https://github.com/bow-swift/bow.git", .exact("0.8.0")), 16 | ], 17 | 18 | targets: [ 19 | // Library targets 20 | .target(name: "BowArch", 21 | dependencies: [.product(name: "Bow", package: "Bow"), 22 | .product(name: "BowEffects", package: "Bow"), 23 | .product(name: "BowOptics", package: "Bow")]), 24 | // Test targets 25 | .testTarget(name: "BowArchTests", 26 | dependencies: [.target(name: "BowArch")]) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Bow Arch](assets/header-bow-arch.png) 2 | 3 | 10 | 11 | Welcome to Bow Arch! 12 | 13 | **Bow Arch** is a library to [architect applications in pure Functional Programming](https://arch.bow-swift.io/docs/quick-start/getting-started/), based on the notion of [Comonadic User Interfaces](https://arch.bow-swift.io/docs/background/comonadic-uis/). Please, refer to the [project website](https://arch.bow-swift.io) for extensive documentation. 14 | 15 |   16 | 17 | ## 👩‍🏫 Principles 18 | 19 | 🎨 **View as a function of state**: Using SwiftUI, we can create user interfaces in a declarative manner, that are a representation of a given state. The library goes even further and promotes the creation of views that are based on immutable state. 20 | 21 | 🚧 **Clear separation of concerns**: The core concepts in the library are [state](https://arch.bow-swift.io/docs/core-concepts/state-and-input/), [input](https://arch.bow-swift.io/docs/core-concepts/state-and-input/), [dispatcher](https://arch.bow-swift.io/docs/core-concepts/dispatcher/), [view](https://arch.bow-swift.io/docs/core-concepts/view/), and [component](https://arch.bow-swift.io/docs/core-concepts/component/). Each one of them deals with a specific concern, and lets us separate how our code deals with different aspects of application development. 22 | 23 | 📦 **Modularity**: The library promotes the [creation of components](https://arch.bow-swift.io/docs/patterns/creating-a-single-component/) that can be easily reused across the application, or even in other applications. These components are highly composable and let us manage the complexity of large applications. 24 | 25 | ✅ **Testability**: Functional code is intrinsically testable; therefore, software created with Bow Arch is easy to test. The library also provides utilities that you can leverage to write powerful and expressive tests. 26 | 27 | 🧩 **Highly polymorphic**: The library is based on abstract, parameterized artifacts. This makes this library not only a library to architect your application, but a library to create different architectures by replacing each of these parameters. Nevertheless, specific bindings are provided in the library, so that users do not have to deal with these details. 28 | 29 | 🧮 **Mathematical background**: Bow Arch is based on concepts from Category Theory, which brings soundness to the reasoning we can do about our code. Nonetheless, the API of the library hides the complexity of these concepts and users do not need to be experts in this topic to use the library in their applications. 30 | 31 |   32 | 33 | ## 💻 How to get it 34 | 35 | Bow Arch is available through Swift Package Manager, integrated in Xcode. You only need to use the repository URL on GitHub and the version or branch you would like to use. Alternatively, you can describe this dependency in your `Package.swift` file by adding the line: 36 | 37 | ```swift 38 | .package(url: "https://github.com/bow-swift/bow-arch.git", from: "{version}") 39 | ``` 40 | 41 |   42 | 43 | ## 👨‍💻 Usage 44 | 45 | Bow Arch lets you architect your application in terms of components that can be reused across applications. Let's go over what you need to create in order to build a stepper component. 46 | 47 | ### 📋 State 48 | 49 | A component should have a state that is rendered in its view. State is usually modeled using an immutable data structure, typically a `struct`. 50 | 51 | For our stepper component, we can model our state as: 52 | 53 | ```swift 54 | struct StepperState { 55 | let count: Int 56 | } 57 | ``` 58 | 59 | ### 📲 Input 60 | 61 | Next step is modeling inputs that a component can handle. Inputs are usually described using cases of an `enum`. 62 | 63 | In our ongoing example, the component can receive two inputs, corresponding to tapping on the decrement or increment buttons. These can be modeled as: 64 | 65 | ```swift 66 | enum StepperInput { 67 | case tapDecrement 68 | case tapIncrement 69 | } 70 | ``` 71 | 72 | ### 🎨 View 73 | 74 | With state and input defined, we can render a view using SwiftUI. SwiftUI is a declarative framework to describe user interfaces in Swift, with multiple bindings for the different operating systems in the Apple Platforms. 75 | 76 | We can describe the view as a function of its state, and use a function to receive inputs: 77 | 78 | ```swift 79 | import SwiftUI 80 | 81 | struct StepperView: View { 82 | let state: StepperState 83 | let handle: (StepperInput) -> Void 84 | 85 | var body: some View { 86 | HStack { 87 | Button("-") { 88 | self.handle(.tapDecrement) 89 | } 90 | 91 | Text("\(state.count)") 92 | 93 | Button("+") { 94 | self.handle(.tapIncrement) 95 | } 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | ### 🔨 Dispatcher 102 | 103 | Inputs new to be transformed into actions that modify the state. This is done at the Dispatcher. Dispatchers are pure functions that receive inputs and produce actions: 104 | 105 | ```swift 106 | typealias StepperDispatcher = StateDispatcher 107 | 108 | let stepperDispatcher = StepperDispatcher.pure { input in 109 | switch input { 110 | case .tapDecrement: 111 | return .modify { state in 112 | StepperState(count: state.count - 1) 113 | }^ 114 | 115 | case .tapIncrement: 116 | return .modify { state in 117 | StepperState(count: state.count + 1) 118 | }^ 119 | } 120 | } 121 | ``` 122 | 123 | ### 🧩 Component 124 | 125 | Finally, we can put everything together as a component: 126 | 127 | ```swift 128 | typealias StepperComponent = StoreComponent 129 | 130 | let stepperComponent = StepperComponent( 131 | initialState: StepperState(count: 0), 132 | dispatcher: stepperDispatcher, 133 | render: StepperView.init) 134 | ``` 135 | 136 | Components already conform to SwiftUI `View`, so they can be used as part of other views, or assigned as the root view of a `UIHostingController`. 137 | 138 | ```swift 139 | let controller = UIHostingController(rootView: stepperComponent) 140 | ``` 141 | 142 |   143 | 144 | ## 👏 Acknowledgements 145 | 146 | We want to thank [Arthur Xavier](https://github.com/arthurxavierx/purescript-comonad-rss/blob/master/RealWorldAppComonadicUI.pdf), [Phil Freeman](https://functorial.com/the-future-is-comonadic/main.pdf), and [Edward Kmett](https://hackage.haskell.org/package/comonad), for their previous work on Comonads and Comonadic UIs in the PureScript and Haskell languages. The usage of optics to break down and compose components is inspired by the use of index and case paths in the [Swift Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) by Stephen Cellis and Brandon Williams. Their work has inspired the creation of this library. 147 | 148 |   149 | 150 | ## ⚖️ License 151 | 152 | Copyright (C) 2020-2021 The Bow Authors 153 | 154 | Licensed under the Apache License, Version 2.0 (the "License"); 155 | you may not use this file except in compliance with the License. 156 | You may obtain a copy of the License at 157 | 158 | http://www.apache.org/licenses/LICENSE-2.0 159 | 160 | Unless required by applicable law or agreed to in writing, software 161 | distributed under the License is distributed on an "AS IS" BASIS, 162 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 163 | See the License for the specific language governing permissions and 164 | limitations under the License. 165 | -------------------------------------------------------------------------------- /Sources/BowArch/ActionMoore/ActionDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | typealias EffectActionDispatcher = EffectDispatcher, E, Input> 5 | -------------------------------------------------------------------------------- /Sources/BowArch/ActionMoore/ActionHandler.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | typealias EffectActionHandler = EffectHandler> 5 | 6 | extension EffectActionHandler { 7 | func focus(_ f: @escaping (AA) -> A) 8 | -> EffectActionHandler 9 | where M == ActionPartial { 10 | 11 | self.lift { action in 12 | action^.mapAction(f) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/BowArch/ActionMoore/MooreComponent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Bow 3 | import BowEffects 4 | 5 | typealias EffectMooreComponent = EffectComponentView, ActionPartial, V> 6 | 7 | extension EffectMooreComponent { 8 | // init( 9 | // initialState: S, 10 | // environment: E, 11 | // reducer: Reducer, 12 | // render: @escaping (S, EffectActionHandler) -> V) 13 | // where W == MoorePartial, 14 | // M == ActionPartial { 15 | // self.init(Moore.from( 16 | // initialState: initialState, 17 | // render: { state in 18 | // UI { send in 19 | // render(state, EffectActionHandler(send)) 20 | // } 21 | // }, 22 | // update: reducer.run)) 23 | // } 24 | // 25 | // init( 26 | // initialState: S, 27 | // reducer: Reducer, 28 | // render: @escaping (S, EffectActionHandler) -> V) 29 | // where W == MoorePartial, 30 | // M == ActionPartial { 31 | // self.init( 32 | // initialState: initialState, 33 | // environment: (), 34 | // reducer: reducer, 35 | // render: render) 36 | // } 37 | } 38 | 39 | extension EffectMooreComponent { 40 | func moore() -> Moore> 41 | where W == MoorePartial, 42 | M == ActionPartial { 43 | self.component.wui^ 44 | } 45 | } 46 | 47 | //public extension EffectMooreComponent { 48 | // func lift( 49 | // _ handler: EffectActionHandler, 50 | // _ f: @escaping (A) -> B 51 | // ) -> EffectMooreComponent 52 | // where W == MoorePartial, 53 | // M == ActionPartial { 54 | // EffectMooreComponent(self.component.lift(handler.focus(f))) 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /Sources/BowArch/Core/Component.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Bow 3 | import BowEffects 4 | 5 | public final class EffectComponent: ObservableObject, Equatable { 6 | @Published var wui: Kind> 7 | let pairing: Pairing 8 | 9 | public init(_ component: Kind>, _ pairing: Pairing) { 10 | self.wui = component 11 | self.pairing = pairing 12 | } 13 | 14 | public func explore(onEffect eff: @escaping (EffectComponent) -> Kind) -> A { 15 | self.explore(write: { c in 16 | Eff.later(.main) { self.wui = c.wui } 17 | .flatTap { eff(c) } 18 | }) 19 | } 20 | 21 | public func explore(write: @escaping (EffectComponent) -> Kind) -> A { 22 | self.wui.extract().make { base in 23 | base.flatMap { action in 24 | write(EffectComponent(self.pairing.select(action, self.wui.duplicate()), self.pairing)) 25 | } 26 | } 27 | } 28 | 29 | public func lift( 30 | _ wf: FunctionK, 31 | _ mf: FunctionK, 32 | _ pairing: Pairing 33 | ) -> EffectComponent { 34 | EffectComponent( 35 | wf.invoke(self.wui.map { ui in 36 | ui.lift(mf.invoke) 37 | }), 38 | pairing 39 | ) 40 | } 41 | 42 | public func handling( 43 | with handler: EffectHandler 44 | ) -> EffectComponent { 45 | EffectComponent(self.wui.map { ui in 46 | ui.handling(with: handler) 47 | }, self.pairing) 48 | } 49 | 50 | public func onEffect(_ eff: @escaping (EffectComponent) -> Kind) -> EffectComponent { 51 | EffectComponent(self.wui.coflatMap { wa in 52 | self.effect { wa in eff(EffectComponent(wa, self.pairing)) } 53 | }, self.pairing) 54 | } 55 | 56 | public func onEffectAction(_ eff: @escaping (EffectComponent, Kind) -> Kind) -> EffectComponent { 57 | EffectComponent(self.wui.coflatMap { wa in 58 | self.effectAction { wa, action in 59 | eff(EffectComponent(wa, self.pairing), action) 60 | } 61 | }, self.pairing) 62 | } 63 | 64 | private func effect(_ eff: @escaping (Kind>) -> Kind) -> UI { 65 | UI { handler in 66 | self.wui.extract().make { base in 67 | let action = Kind>.var() 68 | 69 | let event = binding( 70 | action <- base, 71 | |<-eff(self.pairing.select(action.get, self.wui.duplicate())), 72 | yield: action.get) 73 | 74 | return handler.handle(event) 75 | } 76 | } 77 | } 78 | 79 | private func effectAction(_ eff: @escaping (Kind>, Kind) -> Kind) -> UI { 80 | UI { handler in 81 | self.wui.extract().make { base in 82 | let action = Kind>.var() 83 | 84 | let event = binding( 85 | action <- base, 86 | |<-eff(self.pairing.select(action.get, self.wui.duplicate()), action.get), 87 | yield: action.get) 88 | 89 | return handler.handle(event) 90 | } 91 | } 92 | } 93 | } 94 | 95 | public func ==( 96 | lhs: EffectComponent, 97 | rhs: EffectComponent 98 | ) -> Bool { 99 | lhs === rhs 100 | } 101 | 102 | public extension EffectComponent { 103 | func store() -> Store> 104 | where W == StorePartial, 105 | M == StatePartial { 106 | self.wui^ 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/BowArch/Core/ComponentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Bow 3 | import BowEffects 4 | 5 | public struct EffectComponentView: View { 6 | @ObservedObject var component: EffectComponent 7 | 8 | public init(_ component: EffectComponent) { 9 | self.component = component 10 | } 11 | 12 | public var body: some View { 13 | component.explore(onEffect: { _ in Eff.lazy() }) 14 | } 15 | 16 | public func onEffect(_ eff: @escaping (EffectComponent) -> Kind) -> EffectComponentView { 17 | EffectComponentView(component.onEffect(eff)) 18 | } 19 | 20 | public func onEffectAction(_ eff: @escaping (EffectComponent, Kind) -> Kind) -> EffectComponentView { 21 | EffectComponentView(component.onEffectAction(eff)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/BowArch/Core/Dispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | public struct EffectDispatcher { 5 | private let f: (I) -> [Kleisli>] 6 | 7 | public init(_ f: @escaping (I) -> [Kleisli>]) { 8 | self.f = f 9 | } 10 | 11 | public func on(_ input: I) -> [Kleisli>] { 12 | f(input) 13 | } 14 | 15 | public func dispatch(to handler: EffectHandler, environment: E) -> (I) -> Void { 16 | { input in 17 | self.on(input).traverse { eff in 18 | handler.handle(eff^.run(environment)) 19 | }.void() 20 | .runNonBlocking(on: .global(qos: .background)) 21 | } 22 | } 23 | 24 | public func widen( 25 | _ h: @escaping (E2) -> E, 26 | _ g: @escaping (Kind) -> Kind, 27 | _ f: @escaping (I2) -> I? 28 | ) -> EffectDispatcher { 29 | self.contramap(f).lift(g).lift(h) 30 | } 31 | 32 | public func contramap(_ f: @escaping (I2) -> I?) -> EffectDispatcher { 33 | EffectDispatcher { i in 34 | if let input = f(i) { 35 | return self.on(input) 36 | } else { 37 | return [] 38 | } 39 | } 40 | } 41 | 42 | public func lift(_ f: @escaping (Kind) -> Kind) -> EffectDispatcher { 43 | EffectDispatcher { input in 44 | self.on(input).map { kleisli in 45 | kleisli.map(f)^ 46 | } 47 | } 48 | } 49 | 50 | public func lift(_ f: @escaping (Kleisli>) -> Kleisli>) -> EffectDispatcher { 51 | EffectDispatcher { input in 52 | self.on(input).map(f) 53 | } 54 | } 55 | 56 | public func lift(_ f: @escaping (E2) -> E) -> EffectDispatcher { 57 | EffectDispatcher { input in 58 | self.on(input).map { kleisli in 59 | kleisli.contramap(f) 60 | } 61 | } 62 | } 63 | } 64 | 65 | extension EffectDispatcher: Semigroup { 66 | public func combine(_ other: EffectDispatcher) -> EffectDispatcher { 67 | EffectDispatcher { input in 68 | self.on(input) + other.on(input) 69 | } 70 | } 71 | } 72 | 73 | extension EffectDispatcher: Monoid { 74 | public static func empty() -> EffectDispatcher { 75 | EffectDispatcher { _ in [] } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/BowArch/Core/Handler.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | public class EffectHandler { 5 | private let f: (Kind>) -> Kind 6 | 7 | public init(_ f: @escaping (Kind>) -> Kind) { 8 | self.f = f 9 | } 10 | 11 | public func handle(_ eff: Kind>) -> Kind { 12 | f(eff) 13 | } 14 | 15 | public func lift(_ f: @escaping (Kind) -> Kind) -> EffectHandler { 16 | EffectHandler { action in 17 | self.f(action.map(f)) 18 | } 19 | } 20 | 21 | public func lift(_ f: @escaping (Kind>) -> Kind>) -> EffectHandler { 22 | EffectHandler { action in 23 | self.f(f(action)) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/BowArch/Core/Reducer.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowOptics 3 | 4 | public struct Reducer { 5 | let f: (State, Input) -> State 6 | 7 | public init(_ f: @escaping (State, Input) -> State) { 8 | self.f = f 9 | } 10 | 11 | public func run(_ state: State, _ input: Input) -> State { 12 | f(state, input) 13 | } 14 | 15 | public func focus( 16 | _ lens: Lens, 17 | _ prism: Prism 18 | ) -> Reducer { 19 | Reducer { state, input in 20 | guard let childInput = prism.getOptional(input) else { 21 | return state 22 | } 23 | let newState = self.run(lens.get(state), childInput) 24 | return lens.set(state, newState) 25 | } 26 | } 27 | 28 | public func focus( 29 | _ keyPath: WritableKeyPath, 30 | _ embed: @escaping (Input) -> ParentInput 31 | ) -> Reducer { 32 | focus(ParentState.lens(for: keyPath), ParentInput.prism(for: embed)) 33 | } 34 | 35 | public func focus( 36 | _ lens: Lens 37 | ) -> Reducer { 38 | focus(lens, .identity) 39 | } 40 | 41 | public func focus( 42 | _ keyPath: WritableKeyPath 43 | ) -> Reducer { 44 | focus(ParentState.lens(for: keyPath), .identity) 45 | } 46 | 47 | public func focus( 48 | _ prism: Prism 49 | ) -> Reducer { 50 | focus(.identity, prism) 51 | } 52 | 53 | public func focus( 54 | _ embed: @escaping (Input) -> ParentInput 55 | ) -> Reducer { 56 | focus(.identity, ParentInput.prism(for: embed)) 57 | } 58 | } 59 | 60 | extension Reducer: Semigroup { 61 | public func combine(_ other: Reducer) -> Reducer { 62 | Reducer { state, input in 63 | let newState = self.run(state, input) 64 | return other.run(newState, input) 65 | } 66 | } 67 | } 68 | 69 | extension Reducer: Monoid { 70 | public static func empty() -> Reducer { 71 | Reducer { state, _ in state } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/BowArch/Core/UI.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | public final class UI { 5 | let makeView: (EffectHandler) -> A 6 | 7 | public init(_ makeView: @escaping (EffectHandler) -> A) { 8 | self.makeView = makeView 9 | } 10 | 11 | public func lift(_ f: @escaping (Kind) -> Kind) -> UI { 12 | UI { handler in 13 | self.makeView(handler.lift(f)) 14 | } 15 | } 16 | 17 | public func lift(_ f: @escaping (Kind>) -> Kind>) -> UI { 18 | UI { handler in 19 | self.makeView(handler.lift(f)) 20 | } 21 | } 22 | 23 | public func make(_ f: @escaping (Kind>) -> Kind) -> A { 24 | self.makeView(EffectHandler(f)) 25 | } 26 | 27 | public func handling( 28 | with handler: EffectHandler 29 | ) -> UI { 30 | UI { _ in 31 | self.make { action in 32 | handler.handle(action) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/BowArch/IO/Arch+IO.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | import SwiftUI 4 | 5 | public typealias ComponentView = EffectComponentView, W, M, V> 6 | 7 | // MARK: State-Store 8 | 9 | public typealias StateTHandler = EffectStateTHandler, M, State> 10 | public typealias StateHandler = EffectStateHandler, State> 11 | 12 | public typealias StateTDispatcher = EffectStateTDispatcher, M, E, State, Input> 13 | public typealias StateDispatcher = EffectStateDispatcher, E, State, Input> 14 | 15 | //public typealias StoreTComponent = EffectStoreTComponent, W, M, S, I, V> 16 | public typealias StoreComponent = EffectStoreComponent, E, S, I, V> 17 | 18 | // MARK: Writer-Traced 19 | 20 | typealias WriterTHandler = EffectWriterTHandler, M, State> 21 | typealias WriterHandler = EffectWriterHandler, State> 22 | 23 | typealias WriterTDispatcher = EffectWriterTDispatcher, M, E, State, Input> 24 | typealias WriterDispatcher = EffectWriterDispatcher, E, State, Input> 25 | 26 | typealias TracedTComponent = EffectTracedTComponent, W, M, State, V> 27 | typealias TracedComponent = EffectTracedComponent, State, V> 28 | 29 | // MARK: Action-Moore 30 | 31 | typealias ActionHandler = EffectActionHandler, Action> 32 | 33 | typealias ActionDispatcher = EffectActionDispatcher, E, Action, Input> 34 | 35 | typealias MooreComponent = EffectMooreComponent, Action, V> 36 | -------------------------------------------------------------------------------- /Sources/BowArch/StateStore/StateDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowOptics 3 | import BowEffects 4 | 5 | public typealias EffectStateTDispatcher = EffectDispatcher, E, I> 6 | public typealias EffectStateDispatcher = EffectStateTDispatcher 7 | 8 | public extension EffectStateDispatcher { 9 | static func pure( 10 | _ f: @escaping (I) -> State 11 | ) -> EffectDispatcher 12 | where M == StatePartial { 13 | .effectful { input in Kleisli { _ in Eff.pure(f(input)) } } 14 | } 15 | 16 | static func effectful( 17 | _ f: @escaping (I) -> Kleisli> 18 | ) -> EffectDispatcher 19 | where M == StatePartial { 20 | .workflow { input in [f(input)] } 21 | } 22 | 23 | static func workflow( 24 | _ f: @escaping (I) -> [Kleisli>] 25 | ) -> EffectDispatcher 26 | where M == StatePartial { 27 | EffectDispatcher { input in 28 | f(input).map { action in action.map(id)^ } 29 | } 30 | } 31 | 32 | func widen( 33 | transformEnvironment f: @escaping (E2) -> E, 34 | transformState lens: Lens, 35 | transformInput prism: Prism 36 | ) -> EffectStateDispatcher 37 | where M == StatePartial { 38 | self.widen(f, { state in state^.focus(lens) }, prism.getOptional) 39 | } 40 | 41 | func widen( 42 | transformEnvironment f: @escaping (E2) -> E 43 | ) -> EffectStateDispatcher 44 | where M == StatePartial { 45 | self.widen( 46 | transformEnvironment: f, 47 | transformState: Lens.identity, 48 | transformInput: Prism.identity) 49 | } 50 | 51 | func widen( 52 | transformState lens: Lens 53 | ) -> EffectStateDispatcher 54 | where M == StatePartial { 55 | self.widen( 56 | transformEnvironment: id, 57 | transformState: lens, 58 | transformInput: Prism.identity) 59 | } 60 | 61 | func widen( 62 | transformInput prism: Prism 63 | ) -> EffectStateDispatcher 64 | where M == StatePartial { 65 | self.widen( 66 | transformEnvironment: id, 67 | transformState: Lens.identity, 68 | transformInput: prism) 69 | } 70 | 71 | func widen( 72 | transformEnvironment f: @escaping (E2) -> E, 73 | transformState lens: Lens 74 | ) -> EffectStateDispatcher 75 | where M == StatePartial { 76 | self.widen( 77 | transformEnvironment: f, 78 | transformState: lens, 79 | transformInput: Prism.identity) 80 | } 81 | 82 | func widen( 83 | transformEnvironment f: @escaping (E2) -> E, 84 | transformInput prism: Prism 85 | ) -> EffectStateDispatcher 86 | where M == StatePartial { 87 | self.widen( 88 | transformEnvironment: f, 89 | transformState: Lens.identity, 90 | transformInput: prism) 91 | } 92 | 93 | func widen( 94 | transformState lens: Lens, 95 | transformInput prism: Prism 96 | ) -> EffectStateDispatcher 97 | where M == StatePartial { 98 | self.widen( 99 | transformEnvironment: id, 100 | transformState: lens, 101 | transformInput: prism) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/BowArch/StateStore/StateHandler.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | import BowOptics 4 | 5 | public typealias EffectStateTHandler = EffectHandler> 6 | public typealias EffectStateHandler = EffectStateTHandler 7 | 8 | public extension EffectHandler { 9 | func narrow( 10 | _ lens: Lens 11 | ) -> EffectStateHandler 12 | where M == StatePartial { 13 | self.lift { action in 14 | action^.focus(lens) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/BowArch/StateStore/StoreComponent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Bow 3 | import BowEffects 4 | import BowOptics 5 | 6 | public struct EffectStoreComponent: View { 7 | @ObservedObject var component: EffectComponent, StatePartial, V> 8 | private let initialState: S 9 | private let environment: E 10 | private let dispatcher: EffectStateDispatcher 11 | private let viewBuilder: (S, @escaping (I) -> Void) -> V 12 | private let onEffect: (EffectComponent, StatePartial, V>) -> Kind 13 | 14 | public init( 15 | initialState: S, 16 | environment: E, 17 | dispatcher: EffectStateDispatcher = .empty(), 18 | render: @escaping (S, @escaping (I) -> Void) -> V 19 | ) { 20 | self.init(initialState: initialState, 21 | environment: environment, 22 | dispatcher: dispatcher, 23 | render: render, 24 | onEffect: { _ in Eff.lazy() }) 25 | } 26 | 27 | private init( 28 | initialState: S, 29 | environment: E, 30 | dispatcher: EffectStateDispatcher = .empty(), 31 | render: @escaping (S, @escaping (I) -> Void) -> V, 32 | onEffect: @escaping (EffectComponent, StatePartial, V>) -> Kind 33 | ) { 34 | self.initialState = initialState 35 | self.environment = environment 36 | self.dispatcher = dispatcher 37 | self.viewBuilder = render 38 | self.onEffect = onEffect 39 | self.component = EffectComponent( 40 | Store(initialState) { state in 41 | UI { handler in 42 | render(state, dispatcher.dispatch(to: handler, environment: environment)) 43 | } 44 | }, 45 | Pairing.pairStateStore()) 46 | } 47 | 48 | public var body: some View { 49 | self.component.explore(onEffect: self.onEffect) 50 | } 51 | 52 | public func using( 53 | _ handle: @escaping (I2) -> Void, 54 | transformInput prism: Prism 55 | ) -> EffectStoreComponent { 56 | EffectStoreComponent( 57 | initialState: self.initialState, 58 | environment: self.environment, 59 | dispatcher: self.dispatcher, 60 | render: { state, _ in 61 | self.viewBuilder( 62 | state, 63 | { i in handle(prism.reverseGet(i)) } ) 64 | }) 65 | } 66 | 67 | public func onEffect(_ eff: @escaping (EffectComponent, StatePartial, V>) -> Kind) -> EffectStoreComponent { 68 | EffectStoreComponent( 69 | initialState: self.initialState, 70 | environment: self.environment, 71 | dispatcher: self.dispatcher, 72 | render: self.viewBuilder, 73 | onEffect: { component in 74 | self.onEffect(component) 75 | .followedBy(eff(component)) 76 | }) 77 | } 78 | } 79 | 80 | public extension EffectStoreComponent { 81 | func store() -> Store, V>>{ 82 | self.component.wui^ 83 | } 84 | } 85 | 86 | public extension EffectStoreComponent where E == Any { 87 | init( 88 | initialState: S, 89 | dispatcher: EffectStateDispatcher = .empty(), 90 | render: @escaping (S, @escaping (I) -> Void) -> V 91 | ) { 92 | self.init( 93 | initialState: initialState, 94 | environment: (), 95 | dispatcher: dispatcher, 96 | render: render) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/BowArch/WriterTraced/TracedComponent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Bow 3 | import BowEffects 4 | 5 | typealias EffectTracedTComponent = EffectComponentView, WriterTPartial, V> 6 | typealias EffectTracedComponent = EffectTracedTComponent 7 | 8 | extension EffectTracedTComponent { 9 | // init( 10 | // environment: Environment, 11 | // pairing: Pairing, 12 | // render: @escaping (State, EffectWriterTHandler) -> V) 13 | // where W == TracedTPartial, 14 | // M == WriterTPartial { 15 | // self.init(TracedT(WW.pure({ state in 16 | // UI { send in 17 | // render(state, 18 | // EffectWriterTHandler(send)) 19 | // } 20 | // })), pairing) 21 | // } 22 | // 23 | // init( 24 | // pairing: Pairing, 25 | // render: @escaping (State, EffectWriterTHandler) -> V) 26 | // where W == TracedTPartial, 27 | // M == WriterTPartial { 28 | // self.init(environment: () as Any, 29 | // pairing: pairing, 30 | // render: render) 31 | // } 32 | } 33 | 34 | extension EffectTracedComponent { 35 | // init( 36 | // environment: Environment, 37 | // render: @escaping (State, EffectWriterHandler) -> V) 38 | // where W == TracedPartial, 39 | // M == WriterPartial { 40 | // self.init(Traced { state in 41 | // UI { send in 42 | // render(state, 43 | // EffectWriterHandler(send)) 44 | // } 45 | // }) 46 | // } 47 | // 48 | // init( 49 | // render: @escaping (State, EffectWriterHandler) -> V) 50 | // where W == TracedPartial, 51 | // M == WriterPartial { 52 | // self.init(environment: () as Any, 53 | // render: render) 54 | // } 55 | } 56 | 57 | //public extension EffectTracedTComponent { 58 | // func tracedT() -> TracedT> 59 | // where W == TracedTPartial, 60 | // M == WriterTPartial { 61 | // self.component.wui^ 62 | // } 63 | //} 64 | // 65 | //public extension EffectTracedComponent { 66 | // func traced() -> Traced> 67 | // where W == TracedPartial, 68 | // M == WriterPartial { 69 | // self.component.wui^ 70 | // } 71 | //} 72 | -------------------------------------------------------------------------------- /Sources/BowArch/WriterTraced/WriterDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | typealias EffectWriterTDispatcher = EffectDispatcher, E, Input> 5 | typealias EffectWriterDispatcher = EffectWriterTDispatcher 6 | -------------------------------------------------------------------------------- /Sources/BowArch/WriterTraced/WriterHandler.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | 4 | typealias EffectWriterTHandler = EffectHandler> 5 | typealias EffectWriterHandler = EffectWriterTHandler 6 | -------------------------------------------------------------------------------- /Tests/BowArchTests/BowArchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BowArchTests.swift 3 | // BowArchTests 4 | // 5 | // Created by Tomás Ruiz López on 16/03/2020. 6 | // Copyright © 2020 The Bow Authors. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import BowArch 11 | 12 | class BowArchTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Tests/BowArchTests/DispatcherTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Bow 3 | import BowEffects 4 | import BowArch 5 | import BowOptics 6 | 7 | class DispatcherTests: XCTestCase { 8 | enum TestAction { 9 | case increment 10 | case decrement 11 | } 12 | 13 | enum ParentAction: AutoPrism { 14 | case test(TestAction) 15 | case other 16 | } 17 | 18 | struct Count: Equatable { 19 | let count: Int 20 | 21 | static var lens: Lens { 22 | Lens(get: { $0.count }, set: { Count(count: $1) }) 23 | } 24 | } 25 | 26 | let dispatcher = StateDispatcher.pure { input in 27 | switch input { 28 | case .increment: return .modify { x in x + 1 }^ 29 | case .decrement: return .modify { x in x - 1 }^ 30 | } 31 | } 32 | 33 | let noOp = StateDispatcher.workflow { _ in 34 | [ EnvIO { _ in IO.pure(.modify(id)^) } ] 35 | } 36 | 37 | func testDispatcher() { 38 | let result = run(actions: dispatcher.on(.increment), initial: 0) 39 | 40 | XCTAssertEqual(result, 1) 41 | } 42 | 43 | func testLiftDispatcher() { 44 | let lifted = dispatcher.widen( 45 | transformEnvironment: id, 46 | transformState: Count.lens, 47 | transformInput: ParentAction.prism(for: ParentAction.test) 48 | ) 49 | 50 | let result = run(actions: lifted.on(.test(.increment)), initial: Count(count: 0)) 51 | XCTAssertEqual(result, Count(count: 1)) 52 | 53 | let result2 = run(actions: lifted.on(.other), initial: Count(count: 0)) 54 | XCTAssertEqual(result2, Count(count: 0)) 55 | } 56 | 57 | func testCombination() { 58 | let combined = noOp.combine(dispatcher) 59 | let result = run(actions: combined.on(.increment), initial: 0) 60 | XCTAssertEqual(result, 1) 61 | 62 | let reversed = dispatcher.combine(noOp) 63 | let result2 = run(actions: reversed.on(.increment), initial: 0) 64 | XCTAssertEqual(result2, 1) 65 | } 66 | 67 | func testEmpty() { 68 | let empty = StateDispatcher.empty() 69 | let result = run(actions: empty.on(.increment), initial: 0) 70 | XCTAssertEqual(result, 0) 71 | } 72 | 73 | func run(actions: [Kleisli, Any, StateOf>], initial: S) -> S { 74 | actions.reduce(initial) { result, next in 75 | try! next^.map { action in 76 | action^.runS(result) 77 | }^.unsafeRunSync() 78 | } 79 | } 80 | 81 | func testPrism() { 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/BowArchTests/HandlerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Bow 3 | import BowEffects 4 | import BowOptics 5 | import BowArch 6 | 7 | class HandlerTests: XCTestCase { 8 | func handler(ref: IORef) -> StateHandler { 9 | StateHandler { effect in 10 | effect.flatMap { action in 11 | ref.update { x in 12 | action^.runS(x) 13 | } 14 | } 15 | } 16 | } 17 | 18 | func testHandler() { 19 | let ref = IORef.unsafe(0) 20 | let sut = handler(ref: ref) 21 | try! sut.handle(Task.pure(.modify { $0 + 1 }))^.unsafeRunSync() 22 | let result = try! ref.get()^.unsafeRunSync() 23 | 24 | XCTAssertEqual(result, 1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/header-bow-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/bow-arch/b4a96c3bfad32c5dc7ccee1fae490caa453030e9/assets/header-bow-arch.png -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | arch.bow-swift.io 2 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jekyll", ">= 4.0.0" 4 | gem "jazzy", "~> 0.13.1" 5 | # Needed to support swift_versions param 6 | gem "cocoapods", "~> 1.9.1" 7 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.2) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | claide (1.0.3) 17 | cocoapods (1.9.1) 18 | activesupport (>= 4.0.2, < 5) 19 | claide (>= 1.0.2, < 2.0) 20 | cocoapods-core (= 1.9.1) 21 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 22 | cocoapods-downloader (>= 1.2.2, < 2.0) 23 | cocoapods-plugins (>= 1.0.0, < 2.0) 24 | cocoapods-search (>= 1.0.0, < 2.0) 25 | cocoapods-stats (>= 1.0.0, < 2.0) 26 | cocoapods-trunk (>= 1.4.0, < 2.0) 27 | cocoapods-try (>= 1.1.0, < 2.0) 28 | colored2 (~> 3.1) 29 | escape (~> 0.0.4) 30 | fourflusher (>= 2.3.0, < 3.0) 31 | gh_inspector (~> 1.0) 32 | molinillo (~> 0.6.6) 33 | nap (~> 1.0) 34 | ruby-macho (~> 1.4) 35 | xcodeproj (>= 1.14.0, < 2.0) 36 | cocoapods-core (1.9.1) 37 | activesupport (>= 4.0.2, < 6) 38 | algoliasearch (~> 1.0) 39 | concurrent-ruby (~> 1.1) 40 | fuzzy_match (~> 2.0.4) 41 | nap (~> 1.0) 42 | netrc (~> 0.11) 43 | typhoeus (~> 1.0) 44 | cocoapods-deintegrate (1.0.4) 45 | cocoapods-downloader (1.3.0) 46 | cocoapods-plugins (1.0.0) 47 | nap 48 | cocoapods-search (1.0.0) 49 | cocoapods-stats (1.1.0) 50 | cocoapods-trunk (1.5.0) 51 | nap (>= 0.8, < 2.0) 52 | netrc (~> 0.11) 53 | cocoapods-try (1.2.0) 54 | colorator (1.1.0) 55 | colored2 (3.1.2) 56 | concurrent-ruby (1.1.6) 57 | em-websocket (0.5.1) 58 | eventmachine (>= 0.12.9) 59 | http_parser.rb (~> 0.6.0) 60 | escape (0.0.4) 61 | ethon (0.12.0) 62 | ffi (>= 1.3.0) 63 | eventmachine (1.2.7) 64 | ffi (1.12.2) 65 | forwardable-extended (2.6.0) 66 | fourflusher (2.3.1) 67 | fuzzy_match (2.0.4) 68 | gh_inspector (1.1.3) 69 | http_parser.rb (0.6.0) 70 | httpclient (2.8.3) 71 | i18n (0.9.5) 72 | concurrent-ruby (~> 1.0) 73 | jazzy (0.13.3) 74 | cocoapods (~> 1.5) 75 | mustache (~> 1.1) 76 | open4 77 | redcarpet (~> 3.4) 78 | rouge (>= 2.0.6, < 4.0) 79 | sassc (~> 2.1) 80 | sqlite3 (~> 1.3) 81 | xcinvoke (~> 0.3.0) 82 | jekyll (4.0.1) 83 | addressable (~> 2.4) 84 | colorator (~> 1.0) 85 | em-websocket (~> 0.5) 86 | i18n (>= 0.9.5, < 2) 87 | jekyll-sass-converter (~> 2.0) 88 | jekyll-watch (~> 2.0) 89 | kramdown (~> 2.1) 90 | kramdown-parser-gfm (~> 1.0) 91 | liquid (~> 4.0) 92 | mercenary (~> 0.3.3) 93 | pathutil (~> 0.9) 94 | rouge (~> 3.0) 95 | safe_yaml (~> 1.0) 96 | terminal-table (~> 1.8) 97 | jekyll-sass-converter (2.1.0) 98 | sassc (> 2.0.1, < 3.0) 99 | jekyll-watch (2.2.1) 100 | listen (~> 3.0) 101 | json (2.3.0) 102 | kramdown (2.3.0) 103 | rexml 104 | kramdown-parser-gfm (1.1.0) 105 | kramdown (~> 2.0) 106 | liferaft (0.0.6) 107 | liquid (4.0.3) 108 | listen (3.2.1) 109 | rb-fsevent (~> 0.10, >= 0.10.3) 110 | rb-inotify (~> 0.9, >= 0.9.10) 111 | mercenary (0.3.6) 112 | minitest (5.14.0) 113 | molinillo (0.6.6) 114 | mustache (1.1.1) 115 | nanaimo (0.2.6) 116 | nap (1.1.0) 117 | netrc (0.11.0) 118 | open4 (1.3.4) 119 | pathutil (0.16.2) 120 | forwardable-extended (~> 2.6) 121 | public_suffix (4.0.5) 122 | rb-fsevent (0.10.4) 123 | rb-inotify (0.10.1) 124 | ffi (~> 1.0) 125 | redcarpet (3.5.0) 126 | rexml (3.2.4) 127 | rouge (3.18.0) 128 | ruby-macho (1.4.0) 129 | safe_yaml (1.0.5) 130 | sassc (2.3.0) 131 | ffi (~> 1.9) 132 | sqlite3 (1.4.2) 133 | terminal-table (1.8.0) 134 | unicode-display_width (~> 1.1, >= 1.1.1) 135 | thread_safe (0.3.6) 136 | typhoeus (1.4.0) 137 | ethon (>= 0.9.0) 138 | tzinfo (1.2.7) 139 | thread_safe (~> 0.1) 140 | unicode-display_width (1.7.0) 141 | xcinvoke (0.3.0) 142 | liferaft (~> 0.0.6) 143 | xcodeproj (1.16.0) 144 | CFPropertyList (>= 2.3.3, < 4.0) 145 | atomos (~> 0.1.3) 146 | claide (>= 1.0.2, < 2.0) 147 | colored2 (~> 3.1) 148 | nanaimo (~> 0.2.6) 149 | 150 | PLATFORMS 151 | ruby 152 | 153 | DEPENDENCIES 154 | cocoapods (~> 1.9.1) 155 | jazzy (~> 0.13.1) 156 | jekyll (>= 4.0.0) 157 | 158 | BUNDLED WITH 159 | 2.1.4 160 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | #------------------------- 2 | name: Bow-arch 3 | #------------------------- 4 | title: bow-arch # To be used on meta tags mainly 5 | #------------------------- 6 | description: bow-arch 7 | #------------------------- 8 | author: 47 Degrees 9 | keywords: functional-programming, monads, monad-transformers, functional-data-structure, swift, bow, xcode, xcode-playgrounds, playgrounds, fp-types, adt, free-monads, tagless-final, mtl, for-comprehension, category-theory, architecture, mobile, apps, application, ios, macos 10 | #------------------------- 11 | github-owner: bow-swift 12 | github-repo: bow-arch 13 | #------------------------- 14 | url: https://arch.bow-swift.io/ 15 | #------------------------- 16 | markdown: kramdown 17 | sass: 18 | sass_dir: _sass 19 | style: compressed 20 | sourcemap: never 21 | #------------------------- 22 | permalink: pretty 23 | #------------------------- 24 | exclude: ['config.ru', 'Gemfile', 'Gemfile.lock', 'vendor', 'Procfile', 'Rakefile'] 25 | #------------------------- 26 | -------------------------------------------------------------------------------- /docs/_data/features.yml: -------------------------------------------------------------------------------- 1 | content: 2 | - title: Modular 3 | description: Build cohesive components that can be reused. 4 | icon: img/bow-arch-modular.svg 5 | 6 | - title: Composable 7 | description: Divide your application into manageable pieces that can be composed. 8 | icon: img/bow-arch-composable.svg 9 | 10 | - title: Testable 11 | description: Verify the correct behavior of each part of your application in different scenarios. 12 | icon: img/bow-arch-testable.svg 13 | 14 | - title: Declarative 15 | description: Write your user interfaces with the power of SwiftUI. 16 | icon: img/bow-arch-declarative.svg 17 | -------------------------------------------------------------------------------- /docs/_data/menu.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - title: Documentation 3 | url: /docs 4 | 5 | - title: Github 6 | url: https://github.com/bow-swift/bow-arch 7 | 8 | - title: License 9 | url: https://github.com/bow-swift/bow-arch/blob/master/LICENSE 10 | -------------------------------------------------------------------------------- /docs/_includes/_doc.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /docs/_includes/_footer.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /docs/_includes/_head-docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if page.title %} 5 | {% assign pageTitle = site.title | append: ': ' | append: page.title %} 6 | {% else %} 7 | {% assign pageTitle = site.title %} 8 | {% endif %} 9 | 10 | {{ pageTitle }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/_includes/_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if page.title %} 5 | {% assign pageTitle = site.title | append: ': ' | append: page.title %} 6 | {% else %} 7 | {% assign pageTitle = site.title %} 8 | {% endif %} 9 | 10 | {{ pageTitle }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/_includes/_header.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /docs/_includes/_main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {% for item in site.data.features.content %} 5 |
6 | {{ item.title }} 7 |

{{ item.title }}

8 |

{{ item.description }}

9 |
10 | {% endfor %} 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /docs/_includes/_nav.html: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /docs/_includes/_sidebar.html: -------------------------------------------------------------------------------- 1 |
2 | 21 | 100 |
101 | -------------------------------------------------------------------------------- /docs/_layouts/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include _head-docs.html %} 4 | 5 | {% include _sidebar.html %} 6 | {% include _doc.html %} 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/_layouts/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include _head.html %} 4 | 5 | {% include _nav.html %} 6 | {% include _header.html %} 7 | {% include _main.html %} 8 | {% include _footer.html %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/_sass/base/_base.scss: -------------------------------------------------------------------------------- 1 | // Base 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | // Body, html 5 | // ----------------------------------------------- 6 | html { 7 | box-sizing: border-box; 8 | font-size: $base-font-size; 9 | } 10 | 11 | *, 12 | *::after, 13 | *::before { 14 | box-sizing: inherit; 15 | } 16 | 17 | body, 18 | html { 19 | height: 100%; 20 | } 21 | 22 | // Typography 23 | // ----------------------------------------------- 24 | body { 25 | display: flex; 26 | flex-direction: column; 27 | background: $white; 28 | font-family: $base-font-family; 29 | line-height: $base-line-height; 30 | } 31 | 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | font-family: $header-font-family; 39 | font-weight: $font-semibold; 40 | position: relative; 41 | } 42 | 43 | a { 44 | @include links($link-color, $link-color, $link-color, $link-color); 45 | text-decoration: none; 46 | transition: color $base-duration $base-timing; 47 | 48 | &:hover { 49 | text-decoration: underline; 50 | } 51 | } 52 | 53 | hr { 54 | display: block; 55 | border: none; 56 | } 57 | 58 | .title-bold { 59 | font-weight: $font-bold; 60 | margin: 0 $base-point-grid; 61 | } 62 | -------------------------------------------------------------------------------- /docs/_sass/base/_helpers.scss: -------------------------------------------------------------------------------- 1 | // Helpers 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | .wrapper { 5 | padding: 0 ($base-point-grid * 3); 6 | margin: 0 auto; 7 | box-sizing: border-box; 8 | max-width: $bp-xlarge; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /docs/_sass/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | a, 6 | abbr, 7 | acronym, 8 | address, 9 | applet, 10 | article, 11 | aside, 12 | audio, 13 | b, 14 | big, 15 | blockquote, 16 | body, 17 | canvas, 18 | caption, 19 | center, 20 | cite, 21 | code, 22 | dd, 23 | del, 24 | details, 25 | dfn, 26 | div, 27 | dl, 28 | dt, 29 | em, 30 | embed, 31 | fieldset, 32 | figcaption, 33 | figure, 34 | footer, 35 | form, 36 | h1, 37 | h2, 38 | h3, 39 | h4, 40 | h5, 41 | h6, 42 | header, 43 | hgroup, 44 | html, 45 | i, 46 | iframe, 47 | img, 48 | ins, 49 | kbd, 50 | label, 51 | legend, 52 | li, 53 | mark, 54 | menu, 55 | nav, 56 | object, 57 | ol, 58 | output, 59 | p, 60 | pre, 61 | q, 62 | ruby, 63 | s, 64 | samp, 65 | section, 66 | small, 67 | span, 68 | strike, 69 | strong, 70 | sub, 71 | summary, 72 | sup, 73 | table, 74 | tbody, 75 | td, 76 | tfoot, 77 | th, 78 | thead, 79 | time, 80 | tr, 81 | tt, 82 | u, 83 | ul, 84 | var, 85 | video { 86 | margin: 0; 87 | padding: 0; 88 | border: 0; 89 | font-size: 100%; 90 | font: inherit; 91 | vertical-align: baseline; 92 | } 93 | 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | 109 | body { 110 | line-height: 1; 111 | } 112 | 113 | ol, 114 | ul { 115 | list-style: none; 116 | } 117 | 118 | blockquote, 119 | q { 120 | quotes: none; 121 | } 122 | 123 | blockquote { 124 | &::after, 125 | &::before { 126 | content: ""; 127 | content: none; 128 | } 129 | } 130 | 131 | q { 132 | &::after, 133 | &::before { 134 | content: ""; 135 | content: none; 136 | } 137 | } 138 | 139 | table { 140 | border-collapse: collapse; 141 | border-spacing: 0; 142 | } 143 | -------------------------------------------------------------------------------- /docs/_sass/components/_button.scss: -------------------------------------------------------------------------------- 1 | // Buttons 2 | // ---------------------------------------------- 3 | // ---------------------------------------------- 4 | .button { 5 | font-family: $base-font-family; 6 | display: block; 7 | background: none; 8 | border: none; 9 | outline: none; 10 | text-decoration: none; 11 | 12 | &:hover { 13 | cursor: pointer; 14 | } 15 | 16 | > img { 17 | vertical-align: bottom; 18 | } 19 | 20 | &.link-like { 21 | font-size: 1rem; 22 | color: $link-color; 23 | font-weight: $font-regular; 24 | border: none; 25 | padding: 0 0 ($base-point-grid / 2) 0; 26 | margin: 27 | 0 28 | ($base-point-grid * 2) 29 | -5px 30 | 0; 31 | text-transform: none; 32 | 33 | &:hover, 34 | &:active, 35 | &:focus { 36 | text-decoration: none; 37 | box-shadow: 0 2px; 38 | background: none; 39 | 40 | &::after { 41 | background-position-y: 60%; 42 | } 43 | } 44 | } 45 | } 46 | 47 | .close { 48 | height: 28px; 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | width: 32px; 53 | 54 | &::before, 55 | &::after { 56 | background-color: $white; 57 | content: " "; 58 | height: 100%; 59 | left: 98%; 60 | position: absolute; 61 | top: 36%; 62 | width: 2px; 63 | } 64 | 65 | &::before { 66 | transform: rotate(45deg); 67 | } 68 | 69 | &::after { 70 | transform: rotate(-45deg); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/_sass/components/_code.scss: -------------------------------------------------------------------------------- 1 | // Code 2 | // ---------------------------------------------- 3 | // ---------------------------------------------- 4 | code { 5 | font-family: $code-font-family; 6 | } 7 | 8 | p code, 9 | ul code { 10 | padding: 2px $base-point-grid; 11 | background: $background-inline-code; 12 | font-family: $code-font-family; 13 | border-radius: 2px; 14 | } 15 | 16 | .highlight pre { 17 | background: $background-code; 18 | padding: ($base-point-grid * 3); 19 | overflow: auto; 20 | margin-bottom: ($base-point-grid * 2); 21 | } 22 | -------------------------------------------------------------------------------- /docs/_sass/components/_doc.scss: -------------------------------------------------------------------------------- 1 | // Doc content 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | .site-doc { 5 | position: absolute; 6 | left: 290px; 7 | right: 0; 8 | top: 0; 9 | bottom: 0; 10 | transition: left $base-duration $base-timing; 11 | 12 | &.expanded { 13 | left: 0; 14 | } 15 | 16 | .doc-header { 17 | display: flex; 18 | align-items: center; 19 | height: $doc-top-bar-height; 20 | padding: 0 ($base-point-grid * 5) 0 0; 21 | background: $white; 22 | border-bottom: 1px solid $line-separator-color; 23 | position: sticky; 24 | top: 0; 25 | z-index: 2; 26 | 27 | .doc-toggle { 28 | transition: transform $base-duration $base-timing; 29 | 30 | &:hover { 31 | transform: scaleX(1.5); 32 | } 33 | 34 | > * { 35 | padding: ($base-point-grid * 2); 36 | margin: ($base-point-grid * 2); 37 | } 38 | } 39 | 40 | .link-container { 41 | display: flex; 42 | height: 100%; 43 | width: 100%; 44 | justify-content: flex-end; 45 | align-items: center; 46 | 47 | .strong { 48 | font-weight: 600; 49 | } 50 | 51 | .link-item { 52 | margin-left: ($base-point-grid * 2); 53 | 54 | a { 55 | @include links($brand-secondary, $brand-secondary, $brand-secondary, $brand-secondary); 56 | } 57 | 58 | a:hover { 59 | text-decoration: none; 60 | } 61 | } 62 | } 63 | } 64 | 65 | .doc-content { 66 | padding: ($base-point-grid * 2) ($base-point-grid * 4); 67 | 68 | a { 69 | @include links($brand-secondary, $brand-secondary, $brand-secondary, $brand-secondary); 70 | 71 | &:hover { 72 | text-decoration: underline; 73 | } 74 | } 75 | 76 | ul li { 77 | list-style: none; 78 | } 79 | 80 | ul li::before { 81 | content: "\2022"; 82 | color: $brand-secondary; 83 | font-weight: bold; 84 | display: inline-block; 85 | width: 1.2em; 86 | margin-left: -1em; 87 | } 88 | } 89 | 90 | h1 { 91 | font-size: 2.5rem; 92 | } 93 | 94 | h2 { 95 | font-size: 2rem; 96 | } 97 | 98 | h3 { 99 | font-size: 1.5rem; 100 | } 101 | 102 | h4 { 103 | font-size: 1.25rem; 104 | } 105 | 106 | h5 { 107 | font-size: 1.125rem; 108 | } 109 | 110 | h6 { 111 | font-size: 1rem; 112 | } 113 | 114 | h1, 115 | h2, 116 | h3, 117 | h4, 118 | h5, 119 | h6 { 120 | margin: { 121 | top: ($base-point-grid * 3); 122 | bottom: ($base-point-grid * 2); 123 | } 124 | 125 | &:first-child { 126 | margin-top: 0; 127 | } 128 | } 129 | 130 | p { 131 | margin: ($base-point-grid * 2) 0; 132 | 133 | img { 134 | max-width: 100%; 135 | } 136 | } 137 | 138 | ol, 139 | ul { 140 | padding-left: ($base-point-grid * 2); 141 | margin-bottom: ($base-point-grid * 2); 142 | } 143 | 144 | ol li { 145 | list-style: decimal; 146 | } 147 | 148 | .header-link { 149 | position: absolute; 150 | font-size: 0.6em; 151 | left: -2em; 152 | top: -0.15em; 153 | opacity: 0; 154 | padding: 0.8em; 155 | outline: none; 156 | transform: rotate3d(0, 0, 1, 45deg) scale3d(0.5, 0.5, 0.5); 157 | transition: opacity 0.2s ease, transform 0.2s ease; 158 | 159 | &:hover, 160 | &:focus, 161 | &:active { 162 | text-decoration: none; 163 | } 164 | } 165 | 166 | h1:hover, 167 | h2:hover, 168 | h3:hover, 169 | h4:hover, 170 | h5:hover, 171 | h6:hover { 172 | .header-link { 173 | opacity: 1; 174 | transform: rotate3d(0, 0, 1, 45deg) scale3d(1, 1, 1); 175 | } 176 | } 177 | } 178 | 179 | // Responsive 180 | // ----------------------------------------------- 181 | @include bp(medium) { 182 | .site-doc { 183 | left: 0; 184 | 185 | &.expanded { 186 | overflow: hidden; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /docs/_sass/components/_footer.scss: -------------------------------------------------------------------------------- 1 | // Footer 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | #site-footer { 5 | flex: 0 0 0; 6 | padding: ($base-point-grid * 10) 0; 7 | border-top: 1px solid $line-separator-color; 8 | 9 | .footer-flex { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | height: 100%; 14 | 15 | .footer-dev { 16 | width: $column-4; 17 | 18 | a { 19 | text-decoration: underline; 20 | } 21 | } 22 | 23 | .footer-menu { 24 | display: flex; 25 | 26 | li { 27 | &:not(:last-child) { 28 | margin-right: ($base-point-grid * 4); 29 | } 30 | 31 | a { 32 | padding-bottom: 4px; 33 | font-family: $base-font-family; 34 | 35 | &:hover { 36 | text-decoration: none; 37 | border-bottom: 2px solid $brand-secondary; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | // Responsive 46 | // ----------------------------------------------- 47 | 48 | @include bp(medium) { 49 | #site-footer { 50 | .footer-flex { 51 | justify-content: center; 52 | flex-wrap: wrap; 53 | 54 | .footer-dev, 55 | .footer-menu { 56 | width: $column-8; 57 | } 58 | 59 | .footer-dev { 60 | padding-bottom: ($base-point-grid * 4); 61 | margin-bottom: ($base-point-grid * 4); 62 | text-align: center; 63 | border-bottom: 1px solid $line-separator-color; 64 | } 65 | 66 | .footer-menu { 67 | justify-content: space-evenly; 68 | 69 | li { 70 | &:not(:last-child) { 71 | margin-right: ($base-point-grid * 2); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/_sass/components/_header.scss: -------------------------------------------------------------------------------- 1 | // Header 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | 5 | #site-header { 6 | flex: 1 0 auto; 7 | padding: ($base-point-grid * 25) 0 ($base-point-grid * 8) 0; 8 | 9 | .header-flex { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-evenly; 13 | height: 100%; 14 | width: 97%; 15 | 16 | .header-text { 17 | width: 55%; 18 | height: 80%; 19 | justify-content: center; 20 | display: flex; 21 | flex-direction: column; 22 | 23 | h1 { 24 | font-size: 4rem; 25 | line-height: 1.2; 26 | } 27 | 28 | .header-button { 29 | align-self: flex-start; 30 | padding: 12px ($base-point-grid * 6); 31 | margin-top: 27px; 32 | display: inline-block; 33 | font-weight: $font-semibold; 34 | text-transform: uppercase; 35 | color: $white; 36 | background-color: $brand-secondary; 37 | border: 2px solid $brand-secondary; 38 | border-radius: 2px; 39 | font-size: 14px; 40 | position: relative; 41 | transition: 42 | color $base-duration $base-timing, 43 | background-color $base-duration $base-timing; 44 | 45 | &:visited { 46 | color: $white; 47 | } 48 | 49 | &:hover { 50 | text-decoration: none; 51 | color: $brand-secondary; 52 | background: $white; 53 | } 54 | 55 | &:active { 56 | color: $brand-secondary; 57 | background: $white; 58 | } 59 | } 60 | } 61 | 62 | .header-image { 63 | height: 100%; 64 | width: 45%; 65 | text-align: center; 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | 70 | img { 71 | width: 92%; 72 | } 73 | } 74 | } 75 | } 76 | 77 | // Responsive 78 | // ----------------------------------------------- 79 | 80 | @include bp(large) { 81 | #site-header { 82 | .header-flex { 83 | .header-text { 84 | h1 { 85 | font-size: 2.9rem; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | @include bp(medium) { 93 | #site-header { 94 | padding-top: 100px; 95 | 96 | .header-flex { 97 | flex-direction: column-reverse; 98 | 99 | .header-text { 100 | height: 300px; 101 | text-align: center; 102 | width: $column-12; 103 | 104 | .header-button { 105 | align-self: center; 106 | } 107 | 108 | h1 { 109 | font-size: 2.5rem; 110 | } 111 | } 112 | 113 | .header-image { 114 | width: 80%; 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /docs/_sass/components/_main.scss: -------------------------------------------------------------------------------- 1 | // Features 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | 5 | #site-main { 6 | flex: 1 0 auto; 7 | padding: ($base-point-grid * 10) 0; 8 | 9 | .main-flex { 10 | display: flex; 11 | justify-content: space-between; 12 | height: 100%; 13 | 14 | .main-item { 15 | width: $column-4; 16 | text-align: center; 17 | 18 | &:not(:last-child) { 19 | margin-right: $gutter-margin; 20 | } 21 | 22 | img { 23 | margin-bottom: $base-point-grid; 24 | } 25 | 26 | h2 { 27 | margin-bottom: $base-point-grid; 28 | font-size: 1.25rem; 29 | } 30 | } 31 | } 32 | } 33 | 34 | // Responsive 35 | // ----------------------------------------------- 36 | @include bp(medium) { 37 | #site-main { 38 | .main-flex { 39 | flex-direction: column; 40 | 41 | .main-item { 42 | width: $column-12; 43 | 44 | &:not(:last-child) { 45 | margin-right: 0; 46 | margin-bottom: ($base-point-grid * 8); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/_sass/components/_nav.scss: -------------------------------------------------------------------------------- 1 | // Nav 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | #site-nav { 5 | flex: 0 0 auto; 6 | position: fixed; 7 | padding: ($base-point-grid * 5) 0; 8 | width: 100%; 9 | transition: 10 | background-color $base-duration $base-timing, 11 | padding $base-duration $base-timing; 12 | height: ($base-point-grid * 18); 13 | z-index: 2; 14 | 15 | &.nav-scroll { 16 | padding: ($base-point-grid * 2) 0; 17 | background: #{$white}e; 18 | } 19 | 20 | .nav-flex { 21 | display: flex; 22 | justify-content: space-between; 23 | align-items: center; 24 | height: 100%; 25 | 26 | .nav-brand { 27 | display: flex; 28 | align-items: center; 29 | font-family: $base-font-family; 30 | font-size: 2.3rem; 31 | 32 | &:visited, 33 | &:hover, 34 | &:active { 35 | text-decoration: none; 36 | } 37 | 38 | .home-logo { 39 | padding-right: $base-point-grid; 40 | } 41 | 42 | span { 43 | font-size: $base-point-grid * 3; 44 | } 45 | } 46 | 47 | .nav-menu { 48 | position: relative; 49 | 50 | ul { 51 | display: flex; 52 | 53 | .nav-menu-item { 54 | &:not(:last-child) { 55 | margin-right: ($base-point-grid * 5); 56 | } 57 | 58 | a { 59 | padding-bottom: 4px; 60 | font-family: $base-font-family; 61 | 62 | &:hover { 63 | text-decoration: none; 64 | border-bottom: 2px solid $brand-secondary; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | .nav-icon-open { 72 | padding: 16px; 73 | margin: -16px; 74 | display: none; 75 | transition: transform $base-duration $base-timing; 76 | 77 | &:hover { 78 | transform: scaleX(1.5); 79 | } 80 | } 81 | 82 | .nav-icon-close { 83 | display: none; 84 | padding: ($base-point-grid * 3); 85 | position: absolute; 86 | background: rgba($gray-primary, 0.98); 87 | right: 100%; 88 | top: ($base-point-grid * 5); 89 | border-radius: 5px 0px 0px 5px; 90 | 91 | img { 92 | display: block; 93 | transition: transform 0.3s ease; 94 | } 95 | 96 | &:hover img { 97 | transform: rotate(180deg); 98 | } 99 | } 100 | } 101 | } 102 | 103 | // Responsive 104 | // ----------------------------------------------- 105 | @include bp(medium) { 106 | #site-nav { 107 | .nav-flex { 108 | .nav-menu { 109 | font-size: 1.4rem; 110 | position: fixed; 111 | padding: ($base-point-grid * 5) ($base-point-grid * 6); 112 | background: rgba($gray-primary, 0.98); 113 | height: 100%; 114 | right: -100%; 115 | top: 0; 116 | width: 70%; 117 | z-index: 2; 118 | transition: right $base-duration $base-timing; 119 | 120 | &.open { 121 | right: 0; 122 | box-shadow: 0 0 100px $gray-primary; 123 | } 124 | 125 | ul { 126 | flex-direction: column; 127 | 128 | .nav-menu-item { 129 | padding: ($base-point-grid * 2) 0; 130 | 131 | &:not(:last-child) { 132 | margin-right: 0; 133 | } 134 | } 135 | } 136 | } 137 | 138 | .nav-icon-open, 139 | .nav-icon-close { 140 | display: block; 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /docs/_sass/components/_sidebar-menu.scss: -------------------------------------------------------------------------------- 1 | // Sidebar menu 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | 5 | .sidebar-menu { 6 | margin-top: $base-point-grid; 7 | padding: 0; 8 | 9 | .sidebar-menu-item { 10 | display: flex; 11 | flex-direction: column; 12 | position: relative; 13 | 14 | &::before { 15 | content: ""; 16 | position: absolute; 17 | left: 20px; 18 | top: 9px; 19 | height: 48px; 20 | width: 16px; 21 | background-image: url("../img/sidebar-bullet.svg"); 22 | background-repeat: no-repeat; 23 | background-position-x: center; 24 | background-position-y: center; 25 | transition: background-position 0.1s ease; 26 | } 27 | 28 | .sub-menu { 29 | max-height: 0; 30 | transition: max-height 0.3s ease-in-out; 31 | overflow: hidden; 32 | margin-left: 26px; 33 | box-shadow: 1px 0 $line-separator-color inset; 34 | 35 | a { 36 | display: flex; 37 | justify-content: flex-start; 38 | align-items: center; 39 | padding: 13px; 40 | font-size: 1rem; 41 | line-height: 1.2rem; 42 | height: auto; 43 | width: 80%; 44 | margin-left: $base-point-grid * 2; 45 | 46 | &.active { 47 | background-color: $sidebar-active-color; 48 | } 49 | } 50 | } 51 | 52 | a, 53 | button { 54 | box-sizing: border-box; 55 | font-family: $base-font-family; 56 | font-size: 1rem; 57 | display: flex; 58 | justify-content: space-between; 59 | align-items: center; 60 | padding: $base-point-grid * 3 $base-point-grid * 2 $base-point-grid * 3 56px; 61 | line-height: $base-point-grid * 2; 62 | text-align: left; 63 | width: 100%; 64 | color: $white; 65 | 66 | @include links($link-color, $link-color, $link-color, $link-color); 67 | 68 | transition: background $base-duration $base-timing; 69 | 70 | &:hover { 71 | text-decoration: none; 72 | } 73 | } 74 | 75 | &.active { 76 | &::before { 77 | background-image: url("../img/sidebar-bullet-active.svg"); 78 | } 79 | } 80 | 81 | &.open { 82 | .sub-menu { 83 | max-height: 1600px; // This will suffice for +20 entries in a submenu tops 84 | 85 | a { 86 | // Set hover color in different order just for the submenu links 87 | @include links($link-hover, $link-hover, $link-color, $link-color); 88 | 89 | &.active { 90 | color: $link-color; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/_sass/components/_sidebar.scss: -------------------------------------------------------------------------------- 1 | // Sidebar 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | 5 | .site-sidebar { 6 | position: fixed; 7 | border-right: 1px solid $line-separator-color; 8 | width: 290px; 9 | height: 100%; 10 | left: 0; 11 | z-index: 2; 12 | transition: left $base-duration $base-timing; 13 | 14 | &:hover { 15 | overflow: hidden auto; 16 | } 17 | 18 | &.toggled { 19 | left: -100%; 20 | } 21 | 22 | .sidebar-brand { 23 | padding: $base-point-grid * 2 $base-point-grid * 4; 24 | font-family: $header-font-family; 25 | font-size: 18px; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | height: $doc-top-bar-height; 30 | border-bottom: 1px solid $line-separator-color; 31 | 32 | a { 33 | display: flex; 34 | align-items: center; 35 | width: 100%; 36 | transition: none; 37 | 38 | &:visited, 39 | &:hover, 40 | &:active { 41 | text-decoration: none; 42 | } 43 | 44 | .brand-wrapper { 45 | width: auto; 46 | height: 64px; 47 | } 48 | 49 | span { 50 | font-size: $base-point-grid * 3; 51 | z-index: 30; 52 | white-space: nowrap; 53 | } 54 | } 55 | } 56 | 57 | .sidebar-toggle { 58 | display: none; 59 | } 60 | } 61 | 62 | // Responsive 63 | // ----------------------------------------------- 64 | @include bp(medium) { 65 | .site-sidebar { 66 | left: -100%; 67 | width: 100%; 68 | background-color: $background-sidebar; 69 | 70 | &.toggled { 71 | left: 0; 72 | overflow-y: auto; 73 | } 74 | 75 | .sidebar-toggle { 76 | position: absolute; 77 | right: 16px; 78 | padding: 24px 32px; 79 | display: block; 80 | opacity: 0.7; 81 | transition: opacity 0.3s ease, transform 0.3s ease; 82 | 83 | &:hover { 84 | opacity: 1; 85 | transform: rotate(-180deg); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/_sass/components/_table.scss: -------------------------------------------------------------------------------- 1 | table { 2 | font-size: 1rem; 3 | text-align: left; 4 | overflow-x: auto; 5 | 6 | thead { 7 | background-color: $background-code; 8 | } 9 | 10 | th { 11 | font-weight: $font-semibold; 12 | border: 1px solid $line-separator-color; 13 | } 14 | 15 | th, 16 | td { 17 | padding: 11px $base-point-grid * 3; 18 | 19 | &:first-of-type { 20 | padding-left: $base-point-grid * 2; 21 | } 22 | 23 | &:last-of-type { 24 | padding-right: $base-point-grid * 2; 25 | } 26 | } 27 | 28 | tbody { 29 | td { 30 | border: 1px solid $line-separator-color; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/_sass/utils/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | 5 | // Hover 6 | //------------------------------------------------ 7 | @mixin links($link, $visited, $hover, $active) { 8 | & { 9 | color: $link; 10 | 11 | &:visited { 12 | color: $visited; 13 | } 14 | 15 | &:hover { 16 | color: $hover; 17 | } 18 | 19 | &:active, 20 | &:focus { 21 | color: $active; 22 | } 23 | } 24 | } 25 | 26 | // Breakpoint 27 | // ----------------------------------------------- 28 | // ----------------------------------------------- 29 | @mixin bp($point) { 30 | @if $point==xlarge { 31 | @media (max-width: $bp-xlarge) { 32 | @content; 33 | } 34 | } 35 | 36 | @if $point==large { 37 | @media (max-width: $bp-large) { 38 | @content; 39 | } 40 | } 41 | 42 | @if $point==medium { 43 | @media (max-width: $bp-medium) { 44 | @content; 45 | } 46 | } 47 | 48 | @if $point==small { 49 | @media (max-width: $bp-small) { 50 | @content; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/_sass/utils/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | // ----------------------------------------------- 3 | // ----------------------------------------------- 4 | 5 | // ----------------------------------------------- 6 | // Typography 7 | // ----------------------------------------------- 8 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code&family=Muli:wght@400;600;700&display=swap'); 9 | 10 | // Colours 11 | // ----------------------------------------------- 12 | $brand-primary: #1D263F; 13 | $brand-secondary: #F9C95C; 14 | $gray-primary: #E3E5E8; 15 | $white: #fff; 16 | $link-color: $brand-primary; 17 | $link-hover: darken($gray-primary, 35%); 18 | $line-separator-color: #E3E5E8; 19 | $sidebar-active-color: rgba(29, 38, 63, 0.08); 20 | $sidebar-head-active-color: lighten($brand-secondary, 4%); 21 | $background-code: #17213d; 22 | $background-inline-code: #f5f6f7; 23 | $background-sidebar: #f5f6f7; 24 | 25 | // Typography 26 | // ----------------------------------------------- 27 | $base-font-family: 'Muli', sans-serif; 28 | $header-font-family: $base-font-family; 29 | $code-font-family: 'Fira Code', monospace; 30 | //- 31 | $base-font-color: $gray-primary; 32 | $header-font-color: $gray-primary; 33 | //- 34 | $font-regular: 400; 35 | $font-semibold: 600; 36 | $font-bold: 700; 37 | //- 38 | $base-font-size: 16px; 39 | $base-line-height: 1.6; 40 | // Sizes 41 | // ----------------------------------------------- 42 | $base-point-grid: 8px; 43 | $doc-top-bar-height: 108px; 44 | // Animation 45 | // ----------------------------------------------- 46 | $base-duration: 250ms; 47 | $base-timing: ease-in-out; 48 | 49 | // Breakpoint 50 | // ----------------------------------------------- 51 | $bp-small: 480px; 52 | $bp-medium: 768px; 53 | $bp-large: 992px; 54 | $bp-xlarge: 1140px; 55 | 56 | // Grid 57 | // ----------------------------------------------- 58 | $column-1: (1/12*100%); 59 | $column-2: (2/12*100%); 60 | $column-3: (3/12*100%); 61 | $column-4: (4/12*100%); 62 | $column-5: (5/12*100%); 63 | $column-6: (6/12*100%); 64 | $column-7: (7/12*100%); 65 | $column-8: (8/12*100%); 66 | $column-9: (9/12*100%); 67 | $column-10: (10/12*100%); 68 | $column-11: (11/12*100%); 69 | $column-12: (12/12*100%); 70 | $gutter-margin: ($base-point-grid * 4); 71 | 72 | // Border 73 | // ----------------------------------------------- 74 | $border-color: rgba($gray-primary, 0.05); 75 | -------------------------------------------------------------------------------- /docs/_sass/vendors/highlight/dracula.scss: -------------------------------------------------------------------------------- 1 | /* Dracula Theme v1.2.5 2 | * 3 | * https://github.com/zenorocha/dracula-theme 4 | * 5 | * Copyright 2016, All rights reserved 6 | * 7 | * Code licensed under the MIT license 8 | * http://zenorocha.mit-license.org 9 | * 10 | * @author Rob G 11 | * @author Chris Bracco 12 | * @author Zeno Rocha 13 | * @author Piruin Panichphol 14 | * 15 | * This has been modified to adapt the theme to Bow Arch visual identity 16 | */ 17 | 18 | /* 19 | * Variables 20 | */ 21 | 22 | // $dt-gray-dark: #282a36; // Background 23 | // $dt-gray: #44475a; // Current Line & Selection 24 | // $dt-gray-light: #f8f8f2; // Foreground 25 | // $dt-blue: #6272a4; // Comment 26 | // $dt-cyan: #8be9fd; 27 | // $dt-green: #50fa7b; 28 | // $dt-orange: #ffb86c; 29 | // $dt-pink: #ff79c6; 30 | // $dt-purple: #bd93f9; 31 | // $dt-red: #ff5555; 32 | // $dt-yellow: #f1fa8c; 33 | 34 | $dt-gray-dark: $background-code; // Background 35 | $dt-gray: #fff; // Current Line & Selection 36 | $dt-gray-light: #fff; // Foreground 37 | $dt-blue: #6272a4; // Comment 38 | $dt-cyan: $brand-secondary; // code 39 | 40 | $dt-green: #fff; 41 | $dt-orange: #fff; 42 | $dt-pink: lighten($brand-secondary, 20); 43 | $dt-purple: $brand-secondary; 44 | $dt-red: #fff; 45 | $dt-yellow: lighten($brand-secondary, 5); 46 | 47 | /* 48 | * Styles 49 | */ 50 | 51 | .highlight { 52 | background: $dt-gray-dark; 53 | color: $brand-primary; 54 | 55 | .hll, 56 | .s, 57 | .sa, 58 | .sb, 59 | .sc, 60 | .dl, 61 | .sd, 62 | .s2, 63 | .se, 64 | .sh, 65 | .si, 66 | .sx, 67 | .sr, 68 | .s1, 69 | .ss { 70 | color: $dt-yellow; 71 | } 72 | 73 | .go { 74 | color: $dt-gray; 75 | } 76 | 77 | .err, 78 | .g, 79 | .l, 80 | .n, 81 | .x, 82 | .p, 83 | .ge, 84 | .gr, 85 | .gh, 86 | .gi, 87 | .gp, 88 | .gs, 89 | .gu, 90 | .gt, 91 | .ld, 92 | .no, 93 | .nd, 94 | .ni, 95 | .ne, 96 | .nn, 97 | .nx, 98 | .py, 99 | .w, 100 | .bp { 101 | color: $dt-gray-light; 102 | } 103 | 104 | .gh, 105 | .gi, 106 | .gu { 107 | font-weight: bold; 108 | } 109 | 110 | .ge { 111 | text-decoration: underline; 112 | } 113 | 114 | .bp { 115 | // font-style: italic; 116 | } 117 | 118 | .c, 119 | .ch, 120 | .cm, 121 | .cpf, 122 | .c1, 123 | .cs { 124 | color: $dt-blue; 125 | } 126 | 127 | .kd, 128 | .kt, 129 | .nb, 130 | .nl, 131 | .nv, 132 | .vc, 133 | .vg, 134 | .vi, 135 | .vm { 136 | color: $dt-cyan; 137 | } 138 | 139 | .kd, 140 | .nb, 141 | .nl, 142 | .nv, 143 | .vc, 144 | .vg, 145 | .vi, 146 | .vm { 147 | // font-style: italic; 148 | } 149 | 150 | .na, 151 | .nc, 152 | .nf, 153 | .fm { 154 | color: $dt-green; 155 | } 156 | 157 | .k, 158 | .o, 159 | .cp, 160 | .kc, 161 | .kn, 162 | .kp, 163 | .kr, 164 | .nt, 165 | .ow { 166 | color: $dt-pink; 167 | } 168 | 169 | .m, 170 | .mb, 171 | .mf, 172 | .mh, 173 | .mi, 174 | .mo, 175 | .il { 176 | color: $dt-purple; 177 | } 178 | 179 | .gd { 180 | color: $dt-red; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /docs/css/docs.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | // Utils 5 | @import "utils/variables"; 6 | @import "utils/mixins"; 7 | 8 | // Base 9 | @import "base/reset"; 10 | @import "base/base"; 11 | @import "base/helpers"; 12 | 13 | // Components 14 | @import "components/button"; 15 | @import "components/sidebar"; 16 | @import "components/sidebar-menu"; 17 | @import "components/doc"; 18 | @import "components/code"; 19 | @import "components/table"; 20 | 21 | // Vendor 22 | @import "vendors/highlight/dracula"; 23 | -------------------------------------------------------------------------------- /docs/css/styles.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | // Utils 5 | @import "utils/variables"; 6 | @import "utils/mixins"; 7 | 8 | 9 | // Base 10 | @import "base/reset"; 11 | @import "base/base"; 12 | @import "base/helpers"; 13 | 14 | // Components 15 | @import "components/button"; 16 | @import "components/nav"; 17 | @import "components/header"; 18 | @import "components/main"; 19 | @import "components/footer"; 20 | -------------------------------------------------------------------------------- /docs/img/bow-arch-brand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bow-arch-brand 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/img/bow-arch-composable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bow-arch-composable 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/img/bow-arch-declarative.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bow-arch-declarative 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/img/bow-arch-jumbotron-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bow-arch-jumbotron-image 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/img/bow-arch-modular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bow-arch-modular 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/img/bow-arch-testable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bow-arch-testable 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/bow-arch/b4a96c3bfad32c5dc7ccee1fae490caa453030e9/docs/img/favicon.png -------------------------------------------------------------------------------- /docs/img/nav-icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | nav-icon-close 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/img/nav-icon-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | nav-icon-open 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/img/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/bow-arch/b4a96c3bfad32c5dc7ccee1fae490caa453030e9/docs/img/poster.png -------------------------------------------------------------------------------- /docs/img/sidebar-bullet-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sidebar-bullet-active 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/img/sidebar-bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sidebar-bullet 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/img/sidebar-icon-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | sidebar-icon-open 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | --- 4 | -------------------------------------------------------------------------------- /docs/js/docs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Toggle an specific class to the received DOM element. 3 | * @param {string} elemSelector The query selector specifying the target element. 4 | * @param {string} [activeClass='active'] The class to be applied/removed. 5 | */ 6 | function toggleClass(elemSelector, activeClass = 'active') { 7 | const elem = document.querySelector(elemSelector); 8 | if (elem) { 9 | elem.classList.toggle(activeClass); 10 | } 11 | } 12 | 13 | /** 14 | * Toggle specific classes to an array of corresponding DOM elements. 15 | * @param {Array} elemSelectors The query selectors specifying the target elements. 16 | * @param {Array} activeClasses The classes to be applied/removed. 17 | */ 18 | function toggleClasses(elemSelectors, activeClasses) { 19 | elemSelectors.map((elemSelector, idx) => { 20 | toggleClass(elemSelector, activeClasses[idx]); 21 | }); 22 | } 23 | 24 | /** 25 | * Remove active class from siblings DOM elements and apply it to event target. 26 | * @param {Element} element The element receiving the class, and whose siblings will lose it. 27 | * @param {string} [activeClass='active'] The class to be applied. 28 | */ 29 | function activate(element, activeClass = 'active') { 30 | [...element.parentNode.children].map((elem) => elem.classList.remove(activeClass)); 31 | element.classList.add(activeClass); 32 | } 33 | 34 | /** 35 | * Remove active class from siblings parent DOM elements and apply it to element target parent. 36 | * @param {Element} element The element receiving the class, and whose siblings will lose it. 37 | * @param {string} [activeClass='active'] The class to be applied. 38 | */ 39 | function activateParent(element, activeClass = 'active') { 40 | const elemParent = element.parentNode; 41 | activate(elemParent, activeClass); 42 | } 43 | 44 | /** 45 | * Remove active class from siblings parent DOM elements and apply it to element target parent. 46 | * @param {Element} element The element receiving the class, and whose siblings will lose it. 47 | * @param {string} [activeClass='active'] The class to be applied. 48 | */ 49 | function toggleParent(element, activeClass = "active") { 50 | const elemParent = element.parentNode; 51 | if (elemParent) { 52 | elemParent.classList.toggle(activeClass); 53 | } 54 | } 55 | 56 | /** 57 | * This will make the specified elements click event to show/hide the menu sidebar. 58 | */ 59 | function activateToggle() { 60 | const menuToggles = document.querySelectorAll("#menu-toggle, #main-toggle"); 61 | if (menuToggles) { 62 | [...menuToggles].map(elem => { 63 | elem.onclick = e => { 64 | e.preventDefault(); 65 | toggleClass("#site-sidebar", "toggled"); 66 | toggleClass("#site-doc", "expanded"); 67 | }; 68 | }); 69 | } 70 | } 71 | 72 | /** 73 | * This will make the specified elements click event to behave as a menu 74 | * parent entry, or a link, or sometimes both, depending on the context. 75 | */ 76 | function activateMenuNesting() { 77 | const menuParents = document.querySelectorAll(".drop-nested"); 78 | if (menuParents) { 79 | [...menuParents].map(elem => { 80 | elem.onclick = e => { 81 | e.preventDefault(); 82 | toggleParent(elem, "open"); 83 | const elementType = e.currentTarget.tagName.toLowerCase(); 84 | if (elementType === "a") { 85 | const linkElement = e.currentTarget; 86 | const linkElementParent = linkElement.parentNode; 87 | const destination = linkElement.href; 88 | if ( 89 | destination !== window.location.href && 90 | !linkElementParent.classList.contains("active") 91 | ) { 92 | window.location.href = destination; 93 | } 94 | } 95 | }; 96 | }); 97 | } 98 | } 99 | 100 | /** 101 | * Aux function to retrieve repository stars and watchers count info from 102 | * GitHub API and set it on its proper nodes. 103 | */ 104 | async function loadGitHubStats() { 105 | const ghInfo = document.querySelector('meta[property="github-info"]'); 106 | const ghOwner = ghInfo.dataset.githubOwner; 107 | const ghRepo = ghInfo.dataset.githubRepo; 108 | 109 | if (ghOwner && ghRepo) { 110 | const ghAPI = `https://api.github.com/repos/${ghOwner}/${ghRepo}`; 111 | const ghDataResponse = await fetch(ghAPI); 112 | const ghData = await ghDataResponse.json(); 113 | const ghStars = ghData.stargazers_count; 114 | const starsElement = document.querySelector("#stars-count"); 115 | if (starsElement) { 116 | if (ghStars) { 117 | starsElement.textContent = `☆ ${ghStars}`; 118 | } 119 | else { 120 | starsElement.remove(); 121 | } 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * Function to create an anchor with an specific id 128 | * @param {string} id The corresponding id from which the href will be created. 129 | * @returns {Element} The new created anchor. 130 | */ 131 | function anchorForId(id) { 132 | const anchor = document.createElement("a"); 133 | anchor.className = "header-link"; 134 | anchor.href = `#${id}`; 135 | anchor.innerHTML = '🔗'; 136 | return anchor; 137 | } 138 | 139 | /** 140 | * Aux function to retrieve repository stars and watchers count info from 141 | * @param {string} level The specific level to select header from. 142 | * @param {Element} containingElement The element receiving the anchor. 143 | */ 144 | function linkifyAnchors(level, containingElement) { 145 | const headers = containingElement.getElementsByTagName(`h${level}`); 146 | [...headers].map(header => { 147 | if (typeof header.id !== "undefined" && header.id !== "") { 148 | header.append(anchorForId(header.id)); 149 | } 150 | }); 151 | } 152 | 153 | /** 154 | * Go through all headers applying linkify function 155 | */ 156 | function linkifyAllLevels() { 157 | const content = document.querySelector(".doc-content"); 158 | [...Array(7).keys()].map(level => { 159 | linkifyAnchors(level, content); 160 | }); 161 | } 162 | 163 | // Dropdown functions 164 | 165 | /* When the user clicks on the navigation Documentation button, 166 | * toggle between hiding and showing the dropdown content. 167 | */ 168 | function openDropdown(e) { 169 | e.preventDefault(); 170 | e.stopPropagation(); 171 | // Calling close func. in case we're clicking another dropdown with one opened 172 | closeDropdown(e); 173 | const parent = e.target.closest("div[id$='-dropdown']"); 174 | if (parent) { 175 | const dropdown = parent.querySelector(".dropdown-content"); 176 | if (dropdown) { 177 | dropdown.classList.toggle("show"); 178 | if (dropdown.classList.contains("show")) { 179 | document.documentElement.addEventListener("click", closeDropdown); 180 | } 181 | else { 182 | document.documentElement.removeEventListener("click", closeDropdown); 183 | } 184 | } 185 | } 186 | } 187 | 188 | // Close the dropdown if the user clicks (only) outside of it 189 | function closeDropdown(e) { 190 | const dropdown = document.querySelector("div[id$='-dropdown'] > .dropdown-content.show"); 191 | if (dropdown) { 192 | const currentTarget = e.currentTarget || {}; 193 | const currentTargetParent = currentTarget.closest("div[id$='-dropdown']"); 194 | const dropdownParent = dropdown.closest("div[id$='-dropdown']"); 195 | if (currentTargetParent !== dropdownParent) { 196 | dropdown.classList.remove("show"); 197 | } 198 | document.documentElement.removeEventListener("click", closeDropdown); 199 | } 200 | } 201 | 202 | 203 | window.addEventListener("DOMContentLoaded", () => { 204 | activateToggle(); 205 | activateMenuNesting(); 206 | loadGitHubStats(); 207 | linkifyAllLevels(); 208 | }); 209 | -------------------------------------------------------------------------------- /docs/js/main.js: -------------------------------------------------------------------------------- 1 | // This initialization requires that this script is loaded with `defer` 2 | const navElement = document.querySelector("#site-nav"); 3 | 4 | /** 5 | * Toggle an specific class to the received DOM element. 6 | * @param {string} elemSelector The query selector specifying the target element. 7 | * @param {string} [activeClass='active'] The class to be applied/removed. 8 | */ 9 | function toggleClass(elemSelector, activeClass = "active") { 10 | const elem = document.querySelector(elemSelector); 11 | if (elem) { 12 | elem.classList.toggle(activeClass); 13 | } 14 | } 15 | 16 | // Navigation element modification through scrolling 17 | function scrollFunction() { 18 | if (document.documentElement.scrollTop > 0) { 19 | navElement.classList.add("nav-scroll"); 20 | } else { 21 | navElement.classList.remove("nav-scroll"); 22 | } 23 | } 24 | 25 | // Init call 26 | function loadEvent() { 27 | document.addEventListener("scroll", scrollFunction); 28 | } 29 | 30 | // Attach the functions to each event they are interested in 31 | window.addEventListener("load", loadEvent); 32 | --------------------------------------------------------------------------------
2 | 21 |
22 | {{ content }} 23 |
24 |

4 | 5 | 6 | 7 | Gitter 8 | bow-arch Playground 9 |