# Water
**Repository Path**: asphodelus_dev/Water
## Basic Information
- **Project Name**: Water
- **Description**: Composable SwiftUI
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-08-11
- **Last Updated**: 2023-08-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
Water
**Water** - enable you to progressively write functional SwiftUI.
```swift
func CounterView() -> some View {
let count = defValue(0)
return View {
Text("\(count.value)")
HStack {
Button("+1") {
count.value += 1
}
Button("-1") {
count.value -= 1
}
}
}
}
```
---
* [Why use Water?](#why-use-Water)
* [Installation](#installation)
* [Usage](#usage)
* [Composables](#composables)
* [Plugins](#plugins)
* [Middlewares](#middlewares)
* [Integration with existing projects](#integration-with-existing-projects)
* [Examples](#examples)
* [Compare other frameworks](#compare-other-frameworks)
* [Community](#community)
* [Contributors](#contributors)
* [Thanks](#thanks)
* [License](#license)
---
## Why use Water?
As we all know, `SwiftUI` provides a lot of state management tools, such as: `@State`、`@StateObject`、`@Binding` ..., but these tools can be very confusing for a newbie to SwiftUI, what tool to use when exactly? Also, when the project gets complex, you will be disgusted by the screen full of `@` symbols.
Now, let's see what `@` means in Swift:
- *Attribute*: `@main`、`@objc`, `@autoclosure`
- *PropertyWrapper*: `@State`, `@StateObject`
- *Macro*: `@Observable`
For developers, all those `@` usages will place a heavy burden on the development mind.
In my opinion, `@` also don't conform to normal programming syntax and make your code hard to read and maintain!
So I am trying to develop this library - **Water**.
Of course, Water not only solves the above problems, but more importantly guides you through a progressive approach to writing `SwiftUI` code that will help you step-by-step towards your own standalone project.
**Water** design for the following purposes:
- **Clear**: not require confusing `@` symbols
- **Clean**: focus on code logic rather than code style
- **Composable**: reuse your code use `Composable` (`MVVM` not recommend, but support)
- **Freedom**: not constrain the way you write code (`Redux` style not recommend, but support)
- **Maintainable**: easy and visual testing the state logic
## Installation
**Swift Package Manager**
Add the Package url to your `Xcode` Project or `Package.swift`, finally your `Package.swift` manifest should like below:
```swift
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/OpenLyl/Water.git", .branch("main")),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "Water", package: "Water"),
]),
]
)
```
**Cocoapods**
First, add the following entry in your `Podfile`:
```ruby
pod 'Water', :git => 'https://github.com/OpenLyl/Water.git', :branch => 'main'
```
Then run `pod install`.
Finally, don't forget to import the framework with `import Water`.
## Usage
When using **Water**, you only need to consider whether your state is a `value`、 `object` or an `array`.
**define value**
```swift
func UserView() -> some View {
let name = defValue("jack")
let age = defValue(20)
return View {
Text("\(name.value)'s age = \(age.value)")
Button("change age") {
age.value += 1
}
TextField("input your name", text: name.bindable)
}
}
```
**define object**
```swift
struct User {
var name: String
var age: Int
}
```
```swift
func UserView() -> some View {
let user = defReactive(User(name: "jack", age: 20))
return View {
VStack {
Text("user.name = \(user.name)")
Text("user.age = \(user.age)")
VStack {
Button("change name") {
user.name = "rose"
}
Button("change age") {
user.age += 1
}
}
}
}
}
```
**define array**
```swift
func NumberListView() -> some View {
let array = ["1", "2", "3"]
var nextIndex = array.count + 1
let items = defReactive(array)
return View {
VStack {
LazyVStack {
ForEach(items, id: \.self) { item in
Text("the item = \(item)")
}
Text("combined value = \(items.joined(separator: "-|"))")
}
HStack(spacing: 16) {
Button("add item") {
nextIndex += 1
items.append("\(nextIndex)")
}
Button("remove all") {
nextIndex = 0
items.removeAll()
}
Button("clean item") {
nextIndex = 3
items.replace(with: ["1", "2", "3"])
}
}
}
}
}
```
**define watch**
**Water** also has the ability to listen for data changes and quickly select useful states by using `defWatch`.
```swift
func WatchEffectView() -> some View {
let count = defValue(0)
let name = defValue("some name")
defWatchEffect { _ in
// declare a side effect
print("trigger watch effect")
}
defWatch(name) { value, oldValue, _ in
// when name change do something
print("name changed = \(value), old name = \(oldValue)")
}
return View {
Text("the count = \(count.value)")
Button("click me change count") {
count.value += 1
}
Text("the name = \(name.value)")
TextField("name", text: name.bindable)
}
}
```
**define computed**
In most cases, you can use Swift native computed property directly to pick the defined states.
```swift
let user = defineReactive(User(name: "hello", age: 18))
var displayName: String {
"name is \(user.name)"
}
var displayAge: String {
"\(user.age) years old"
}
```
outside of this,**Water** also provide the cacheable computed property, when there are complex data processing, use `defComputed`.
```swift
func FilterNumbersView() -> some View {
let showEven = defValue(false)
let items = defReactive([1, 2, 3, 4, 5, 6])
let evenNumbers = defComputed {
items.filter { !showEven.value || $0 % 2 == 0}
}
return View {
VStack {
Toggle(isOn: showEven.bindable) {
Text("Only show even numbers")
}
Button("dynamic insert num") {
let newNumbers = [7, 8, 9, 10]
items.append(contentsOf: newNumbers)
}
}
.padding(.horizontal, 15)
List(evenNumbers.value, id: \.self) { num in
Text("the num = \(num)")
}
}
}
```
## Composables
Once all the states become reactive, use composable way to extract the data logic is so natural.
**useReducer**
`useReducer` allow you code `SwiftUI` in `Redux` style, very similar to [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture).
```swift
struct CountState {
var count: Int = 0
}
enum CountAction {
case increase
case decrease
}
func countReducer(state: inout CountState, action: CountAction) {
switch action {
case .increase:
state.count += 1
case .decrease:
state.count -= 1
}
}
func ReducerCounterView() -> some View {
let (useCountState, dispatch) = useReducer(CountState(), countReducer)
return View {
Text("the count = \(useCountState().count)")
HStack {
Button("+1") {
dispatch(.increase)
}
Button("-1") {
dispatch(.decrease)
}
}
}
}
```
**useStore**
`useStore` will be more powerful than `useReducer`, it's still under development.
```swift
let useCounterStore = defStore("counter") {
let count = defValue(0)
func increment() {
count.value += 1
}
func decrement() {
count.value -= 1
}
return (count, increment, decrement)
}
func StoreCountView() -> some View {
let store = useCounterStore()
return View {
Text("the count = \(store().count)")
HStack {
Button("+1") {
store.increment()
}
Button("-1") {
store.decrement()
}
}
}
}
```
**useFetch**
`useFetch` provides the ability to send http restful requests and final fetch the network result data, now is a simple version, it will be more flexible and powerful in the future.
```swift
func UseFetchView() -> some View {
let (isFetching, error, data) = useFetch(url: "https://httpbin.org/get")
return View {
VStack {
Text(isFetching.value ? "is fetching" : "fetch completed")
Text("error = \(error.value)")
if let data = data.value, let responseString = String(data: data, encoding: .utf8) {
Text("data is \(responseString)")
}
}
}
}
```
**useAsyncState**
`useAsyncState` provides the ability to use state from existing async context. sometimes, it's more useful than `useFetch`.
```swift
struct Todo: Codable {
let id: Int
let todo: String
let completed: Bool
}
func fetchTodos() async -> [Todo] {
...
}
func UseAsyncStateView() -> some View {
let (state, isLoading) = useAsyncState(fetchTodos, [] as [Todo])
var todos: [Todo] {
state.value
}
return View {
if isLoading.value {
Text("loading...")
} else {
List(todos, id: \.id) { todo in
Text(todo.todo)
}
}
}
}
```
**useEnvironment**
The following code shows how to get the system environment on demand, it's equivalent to `@Environment(\.dismiss) private var dismiss`.
```swift
func UseEnvironmentView() -> some View {
let dismiss = useEnvironment(\.dismiss)
let count = defValue(0)
return View {
VStack {
Text("new value = \(count.value)")
Button("+1") {
count.value += 1
}
Button("-1") {
count.value -= 1
}
Button("dismiss") {
dismiss.value?()
}
}
}
.useEnvironment(\.dismiss)
}
```
can also use `.bindable` to keep sync with system bindable environment.
```swift
func UseEditModeEnvironmentView() -> some View {
let name = defValue("hello word edit mode")
let editMode = defValue(EditMode.inactive)
return View {
Form {
if editMode.value.isEditing == true {
TextField("Name", text: name.bindable)
} else {
Text(name.value)
}
}
.animation(nil, value: editMode.value)
.toolbar {
EditButton()
}
.environment(\.editMode, editMode.bindable)
}
}
```
**useRouter**
`under development`
## Build your own composable
`under writing`
## Plugins
`under development`
## Middlewares
`under development`
## Integration
**use offical struct view style**
``` swift
struct CountereView: View {
let count = defValue(0)
var body: some View {
Water.View { // will change in future
Text("current count = \(count.value)")
HStack {
Button("+") {
count.value += 1
}
Button("-") {
count.value -= 1
}
}
}
}
}
````
**integrate with other SwfitUI views**
`under writing`
## Examples
- [UseCases](./Example/Example/UseCases)
- ValueUseCases
- ReactivityUseCases
- WatchUseCases
- ReducerUseCases
- StoreUseCases
- ComputedUseCases
- ComposableUseCases
- MemoUseCases
- EnvironmentUseCases
- NavigationUseCases
- EffectScopeUseCases
- UseFetchUseCasesView
- [Todos](./Example/Example/Apps/TodoApp)
- [SwiftUI Essentials](./Apps/Landmarks)
- Garden (Mastodon client) - under development
- Other TCA examples - under development
## Compare other frameworks
**compare with swift-composable-architecture**
`under writing`
## Community
If you want to discuss **Water** or have a question about how to use it to solve a particular problem, you can join the discord channel:
[](https://discord.gg/rr5PFXEF4n)
## Thanks
This project is heavily inspired by the following awesome projects.
- [react](https://github.com/facebook/react)
- [vue](https://github.com/vuejs/core)
- [vueuse](https://github.com/vueuse/vueuse)
- [swiftui-hooks](https://github.com/ra1028/swiftui-hooks)
## Contribution
**Water** is only a basic MVP at this point and is not recommended for online products, there are still some areas that need to be worked on, as follows:
- need more util functions to handle reactivity system
- `Composables` is just getting started, need more logic to handle complex situations
- add more unit test and improve the test coverage
- write more use cases with snapshot test
- write more example apps and tutorials
- code with more comments
- performance test
so if you are interested in this project, please join us for something fun!
## License
This library is released under the MIT license. See [LICENSE](https://github.com/OpenLyl/Water/blob/main/LICENSE) for details.