Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 43 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,34 @@ Enhances reusability of stateful logic and gives state and lifecycle to function
## Introducing Hooks

```swift
struct Example: HookView {
var hookBody: some View {
let time = useState(Date())

useEffect(.once) {
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
time.wrappedValue = $0.fireDate
}
func timer() -> some View {
let time = useState(Date())

return {
timer.invalidate()
}
useEffect(.once) {
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
time.wrappedValue = $0.fireDate
}

return Text("Now: \(time.wrappedValue)")
return {
timer.invalidate()
}
}

return Text("Time: \(time.wrappedValue)")
}
```

SwiftUI Hooks is a SwiftUI implementation of React Hooks. Brings the state and lifecycle into the function view, without depending on elements that are only allowed to be used in struct views such as `@State` or `@ObservedObject`.
SwiftUI-Hooks is a SwiftUI implementation of React Hooks. Brings the state and lifecycle into the function view, without depending on elements that are only allowed to be used in struct views such as `@State` or `@ObservedObject`.
It allows you to reuse stateful logic between views by building custom hooks composed with multiple hooks.
Furthermore, hooks such as `useEffect` also solve the problem of lack of lifecycles in SwiftUI.

The API and behavioral specs of SwiftUI Hooks are entirely based on React Hooks, so you can leverage your knowledge of web applications to your advantage.
The API and behavioral specs of SwiftUI-Hooks are entirely based on React Hooks, so you can leverage your knowledge of web applications to your advantage.

There're already a bunch of documentations on React Hooks, so you can refer to it and learn more about Hooks.

- [React Hooks Documentation](https://reactjs.org/docs/hooks-intro.html)
- [Youtube Video](https://www.youtube.com/watch?v=dpw9EHDh2bM)

[Flutter Hooks](https://pub.dev/packages/flutter_hooks) encouraged me to create SwiftUI Hooks, and its well implemented design gives me a lot of inspiration.
Kudos to [@rrousselGit](https://github.com/rrousselGit) and of course [React Team](https://reactjs.org/community/team.html).

---

## Hooks API
Expand All @@ -83,7 +78,10 @@ Triggers a view update when the state has been changed.

```swift
let count = useState(0) // Binding<Int>
count.wrappedValue = 123

Button("Increment") {
count.wrappedValue += 1
}
```

</details>
Expand All @@ -92,19 +90,19 @@ count.wrappedValue = 123
<summary><CODE>useEffect</CODE></summary>

```swift
func useEffect(_ computation: HookComputation, _ effect: @escaping () -> (() -> Void)?)
func useEffect(_ updateStrategy: HookUpdateStrategy? = nil, _ effect: @escaping () -> (() -> Void)?)
```

A hook to use a side effect function that is called the number of times according to the strategy specified by `computation`.
Optionally the function can be cancelled when this hook is unmount from the view tree or when the side-effect function is called again.
Note that the execution is deferred until after all the hooks have been evaluated.
A hook to use a side effect function that is called the number of times according to the strategy specified with `updateStrategy`.
Optionally the function can be cancelled when this hook is disposed or when the side-effect function is called again.
Note that the execution is deferred until after ohter hooks have been updated.

```swift
useEffect(.once) {
print("View is mounted")
useEffect {
print("Do side effects")

return {
print("View is unmounted")
print("Do cleanup")
}
}
```
Expand All @@ -115,16 +113,16 @@ useEffect(.once) {
<summary><CODE>useLayoutEffect</CODE></summary>

```swift
func useLayoutEffect(_ computation: HookComputation, _ effect: @escaping () -> (() -> Void)?)
func useLayoutEffect(_ updateStrategy: HookUpdateStrategy? = nil, _ effect: @escaping () -> (() -> Void)?)
```

A hook to use a side effect function that is called the number of times according to the strategy specified by `computation`.
A hook to use a side effect function that is called the number of times according to the strategy specified with `updateStrategy`.
Optionally the function can be cancelled when this hook is unmount from the view tree or when the side-effect function is called again.
The signature is identical to `useEffect`, but this fires synchronously when the hook is called.

```swift
useLayoutEffect(.always) {
print("View is being evaluated")
useLayoutEffect {
print("Do side effects")
return nil
}
```
Expand All @@ -135,10 +133,10 @@ useLayoutEffect(.always) {
<summary><CODE>useMemo</CODE></summary>

```swift
func useMemo<Value>(_ computation: HookComputation, _ makeValue: @escaping () -> Value) -> Value
func useMemo<Value>(_ updateStrategy: HookUpdateStrategy, _ makeValue: @escaping () -> Value) -> Value
```

A hook to use memoized value preserved until it is re-computed at the timing specified with the `computation`
A hook to use memoized value preserved until it is updated at the timing determined with given `updateStrategy`.

```swift
let random = useMemo(.once) {
Expand All @@ -160,7 +158,10 @@ The essential of this hook is that setting a value to `current` doesn't trigger

```swift
let value = useRef("text") // RefObject<String>
value.current = "new text"

Button("Save text") {
value.current = "new text"
}
```

</details>
Expand All @@ -172,7 +173,7 @@ value.current = "new text"
func useReducer<State, Action>(_ reducer: @escaping (State, Action) -> State, initialState: State) -> (state: State, dispatch: (Action) -> Void)
```

A hook to use the current state computed with the passed `reducer`, and a `dispatch` function to dispatch an action to mutate the compute a new state.
A hook to use the state returned by the passed `reducer`, and a `dispatch` function to send actions to update the state.
Triggers a view update when the state has been changed.

```swift
Expand Down Expand Up @@ -214,11 +215,11 @@ let colorScheme = useEnvironment(\.colorScheme) // ColorScheme
<summary><CODE>usePublisher</CODE></summary>

```swift
func usePublisher<P: Publisher>(_ computation: HookComputation, _ makePublisher: @escaping () -> P) -> AsyncPhase<P.Output, P.Failure>
func usePublisher<P: Publisher>(_ updateStrategy: HookUpdateStrategy, _ makePublisher: @escaping () -> P) -> AsyncPhase<P.Output, P.Failure>
```

A hook to use the most recent phase of asynchronous operation of the passed publisher.
The publisher will be subscribed at the first computation and will be re-subscribed according to the strategy specified with the passed `computation`.
The publisher will be subscribed at the first update and will be re-subscribed according to the given `updateStrategy`.
Triggers a view update when the asynchronous phase has been changed.

```swift
Expand Down Expand Up @@ -510,6 +511,13 @@ Repository: https://github.com/ra1028/SwiftUI-Hooks

---

## Acknowledgements

- [React Hooks](https://reactjs.org/docs/hooks-intro.html)
- [Flutter Hooks](https://github.com/rrousselGit/flutter_hooks)

---

## License

[MIT © Ryo Aoyama](LICENSE)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Hooks/Context/Consumer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI

public extension Context {
/// A view that consumes the context values that provided by `Provider` through view tree.
/// If the value is not provided by the `Provider` from upstream of the view tree, the view's evaluation will be asserted.
/// If the value is not provided by the `Provider` from upstream of the view tree, the view's update will be asserted.
struct Consumer<Content: View>: View {
private let content: (T) -> Content

Expand Down
34 changes: 16 additions & 18 deletions Sources/Hooks/Hook.swift
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
/// `Hook` manages the state and overall behavior of a hook. It has lifecycles to manage the state and when to compute the value.
/// `Hook` manages the state and overall behavior of a hook. It has lifecycles to manage the state and when to update the value.
/// It must be immutable, and should not have any state in itself, but should perform appropriate operations on the state managed by the internal system passed to lifecycle functions.
///
/// Use it when your custom hook becomes too complex can not be made with existing hooks composition.
public protocol Hook {
/// The type of state that preserves the value computed by this hook.
/// The type of state that is used to preserves the value returned by this hook.
associatedtype State = Void

/// The type of value that this hook computes.
/// The type of value that this hook returns.
associatedtype Value

/// The type of contextual information about the state of the hook.
typealias Coordinator = HookCoordinator<Self>

/// Indicates when to compute the value that this hook provides.
var computation: HookComputation { get }
/// A strategy that determines when to update the state.
var updateStrategy: HookUpdateStrategy? { get }

/// Indicates whether the value should be computed after all hooks have been evaluated.
var shouldDeferredCompute: Bool { get }
/// Indicates whether the value should be updated after all hooks have been evaluated.
var shouldDeferredUpdate: Bool { get }

/// Returns a initial state of this hook.
/// Internal system calls this function to create a state at first time each hook is evaluated.
func makeState() -> State

/// Returns a value for each hook call.
/// Updates the state when the `updateStrategy` determines that an update is necessary.
/// - Parameter coordinator: A contextual information about the state of the hook.
func makeValue(coordinator: Coordinator) -> Value
func updateState(coordinator: Coordinator)

/// Compute the value and store it to the state of the hook.
/// The timing at which this function is called is specified by `computation`.
/// Returns a value which is returned when this hook is called.
/// - Parameter coordinator: A contextual information about the state of the hook.
func compute(coordinator: Coordinator)
func value(coordinator: Coordinator) -> Value

/// Dispose of the state and interrupt running asynchronous operation.
func dispose(state: State)
}

public extension Hook {
/// Indicates whether the value should be computed after all hooks have been evaluated.
/// Indicates whether the value should be updated after other hooks have been updated.
/// Default is `false`.
var shouldDeferredCompute: Bool { false }
var shouldDeferredUpdate: Bool { false }

/// Compute the value and store it to the state of the hook.
/// The timing at which this function is called is specified by `computation`.
/// Updates the state when the `updateStrategy` determines that an update is necessary.
/// Does not do anything by default.
/// - Parameter coordinator: A contextual information about the state of the hook.
func compute(coordinator: Coordinator) {}
func updateState(coordinator: Coordinator) {}

/// Dispose of the state and interrupt running asynchronous operation.
/// Does not do anything by default.
Expand All @@ -62,5 +60,5 @@ public extension Hook where Value == Void {
/// Returns a value for each hook call.
/// Default is Void.
/// - Parameter coordinator: A contextual information about the state of the hook.
func makeValue(coordinator: Coordinator) {}
func value(coordinator: Coordinator) {}
}
4 changes: 2 additions & 2 deletions Sources/Hooks/Hook/UseContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public func useContext<T>(_ context: Context<T>.Type) -> T {

private struct ContextHook<T>: Hook {
let context: Context<T>.Type
let computation = HookComputation.once
let updateStrategy: HookUpdateStrategy? = .once

func makeValue(coordinator: Coordinator) -> T {
func value(coordinator: Coordinator) -> T {
guard let value = coordinator.environment[context] else {
fatalError(
"""
Expand Down
40 changes: 20 additions & 20 deletions Sources/Hooks/Hook/UseEffect.swift
Original file line number Diff line number Diff line change
@@ -1,68 +1,68 @@
/// A hook to use a side effect function that is called the number of times according to the strategy specified by `computation`.
/// Optionally the function can be cancelled when this hook is unmount from the view tree or when the side-effect function is called again.
/// Note that the execution is deferred until after all the hooks have been evaluated.
/// A hook to use a side effect function that is called the number of times according to the strategy specified with `updateStrategy`.
/// Optionally the function can be cancelled when this hook is disposed or when the side-effect function is called again.
/// Note that the execution is deferred until after ohter hooks have been updated.
///
/// useEffect(.once) {
/// print("View is mounted")
/// useEffect {
/// print("Do side effects")
///
/// return {
/// print("View is unmounted")
/// print("Do cleanup")
/// }
/// }
///
/// - Parameters:
/// - computation: A computation strategy that to determine when to call the effect function again.
/// - updateStrategy: A strategy that determines when to re-call the given side effect function.
/// - effect: A closure that typically represents a side-effect.
/// It is able to return a closure that to do something when this hook is unmount from the view or when the side-effect function is called again.
public func useEffect(
_ computation: HookComputation,
_ updateStrategy: HookUpdateStrategy? = nil,
_ effect: @escaping () -> (() -> Void)?
) {
useHook(
EffectHook(
computation: computation,
shouldDeferredCompute: true,
updateStrategy: updateStrategy,
shouldDeferredUpdate: true,
effect: effect
)
)
}

/// A hook to use a side effect function that is called the number of times according to the strategy specified by `computation`.
/// A hook to use a side effect function that is called the number of times according to the strategy specified with `updateStrategy`.
/// Optionally the function can be cancelled when this hook is unmount from the view tree or when the side-effect function is called again.
/// The signature is identical to `useEffect`, but this fires synchronously when the hook is called.
///
/// useLayoutEffect(.always) {
/// print("View is being evaluated")
/// useLayoutEffect {
/// print("Do side effects")
/// return nil
/// }
///
/// - Parameters:
/// - computation: A computation strategy that to determine when to call the effect function again.
/// - updateStrategy: A strategy that determines when to re-call the given side effect function.
/// - effect: A closure that typically represents a side-effect.
/// It is able to return a closure that to do something when this hook is unmount from the view or when the side-effect function is called again.
public func useLayoutEffect(
_ computation: HookComputation,
_ updateStrategy: HookUpdateStrategy? = nil,
_ effect: @escaping () -> (() -> Void)?
) {
useHook(
EffectHook(
computation: computation,
shouldDeferredCompute: false,
updateStrategy: updateStrategy,
shouldDeferredUpdate: false,
effect: effect
)
)
}

private struct EffectHook: Hook {
let computation: HookComputation
let shouldDeferredCompute: Bool
let updateStrategy: HookUpdateStrategy?
let shouldDeferredUpdate: Bool
let effect: () -> (() -> Void)?

func makeState() -> State {
State()
}

func compute(coordinator: Coordinator) {
func updateState(coordinator: Coordinator) {
coordinator.state.cleanup = effect()
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Hooks/Hook/UseEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public func useEnvironment<Value>(_ keyPath: KeyPath<EnvironmentValues, Value>)

private struct EnvironmentHook<Value>: Hook {
let keyPath: KeyPath<EnvironmentValues, Value>
let computation = HookComputation.once
let updateStrategy: HookUpdateStrategy? = .once

func makeValue(coordinator: Coordinator) -> Value {
func value(coordinator: Coordinator) -> Value {
coordinator.environment[keyPath: keyPath]
}
}
Loading