├── .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 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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