Composable SwiftUI
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
}
}
}
}
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:
@main
、@objc
, @autoclosure
@State
, @StateObject
@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:
@
symbolsComposable
(MVVM
not recommend, but support)Redux
style not recommend, but support)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
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:
Composables
is just getting started, need more logic to handle complex situationsso 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.