Water - enable you to progressively write functional SwiftUI.
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?
- Installation
- Usage
- Composables
- Plugins
- Middlewares
- Integration with existing projects
- Examples
- Compare with X
- Community
- Contribution
- Thanks
- License
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
Swift Package Manager
Add the Package url to your Xcode
Project or Package.swift
, finally your Package.swift
manifest should like below:
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
:
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
.
When using Water, you only need to consider whether your state is a value
、 object
or an array
.
define value
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
struct User {
var name: String
var age: Int
}
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
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
.
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.
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
.
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)")
}
}
}
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.
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.
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.
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
.
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
.
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.
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
under writing
under development
under development
use official struct view style
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
- UseCases
- ValueUseCases
- ReactivityUseCases
- WatchUseCases
- ReducerUseCases
- StoreUseCases
- ComputedUseCases
- ComposableUseCases
- MemoUseCases
- EnvironmentUseCases
- NavigationUseCases
- EffectScopeUseCases
- UseFetchUseCasesView
- Todos
- SwiftUI Essentials
- Garden (Mastodon client) - under development
- Other TCA examples - under development
compare with swift-composable-architecture
under writing
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:
This project is heavily inspired by the following awesome projects.
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!
This library is released under the MIT license. See LICENSE for details.