A SwiftUI implementation of React Hooks. Enhances reusability of stateful logic and gives state and lifecycle to function view.

Overview

SwiftUI Hooks

A SwiftUI implementation of React Hooks.

Enhances reusability of stateful logic and gives state and lifecycle to function view.

Swift5 Platform GitHub Actions



Introducing Hooks

func timer() -> some View {
    let time = useState(Date())

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

        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.
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.

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


Hooks API

πŸ‘‡ Click to open the description.

useState
func useState<State>(_ initialState: State) -> Binding<State>

A hook to use a Binding<State> wrapping current state to be updated by setting a new state to wrappedValue.
Triggers a view update when the state has been changed.

let count = useState(0)  // Binding<Int>

Button("Increment") {
    count.wrappedValue += 1
}
useEffect
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 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 {
    print("Do side effects")

    return {
        print("Do cleanup")
    }
}
useLayoutEffect
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 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 {
    print("Do side effects")
    return nil
}
useMemo
func useMemo<Value>(_ updateStrategy: HookUpdateStrategy, _ makeValue: @escaping () -> Value) -> Value

A hook to use memoized value preserved until it is updated at the timing determined with given updateStrategy.

let random = useMemo(.once) {
    Int.random(in: 0...100)
}
useRef
func useRef<T>(_ initialValue: T) -> RefObject<T>

A hook to use a mutable ref object storing an arbitrary value.
The essential of this hook is that setting a value to current doesn't trigger a view update.

let value = useRef("text")  // RefObject<String>

Button("Save text") {
    value.current = "new text"
}
useReducer
func useReducer<State, Action>(_ reducer: @escaping (State, Action) -> State, initialState: State) -> (state: State, dispatch: (Action) -> Void)

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.

enum Action {
    case increment, decrement
}

func reducer(state: Int, action: Action) -> Int {
    switch action {
        case .increment:
            return state + 1

        case .decrement:
            return state - 1
    }
}

let (count, dispatch) = useReducer(reducer, initialState: 0)
useEnvironment
func useEnvironment<Value>(_ keyPath: KeyPath<EnvironmentValues, Value>) -> Value

A hook to use environment value passed through the view tree without @Environment property wrapper.

let colorScheme = useEnvironment(\.colorScheme)  // ColorScheme
usePublisher
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 update and will be re-subscribed according to the given updateStrategy.
Triggers a view update when the asynchronous phase has been changed.

let phase = usePublisher(.once) {
    URLSession.shared.dataTaskPublisher(for: url)
}
usePublisherSubscribe
func usePublisherSubscribe<P: Publisher>(_ makePublisher: @escaping () -> P) -> (phase: AsyncPhase<P.Output, P.Failure>, subscribe: () -> Void)

A hook to use the most recent phase of asynchronous operation of the passed publisher, and a subscribe function to be started to subscribe arbitrary timing.
Update the view with the asynchronous phase change.

let (phase, subscribe) = usePublisherSubscribe {
    URLSession.shared.dataTaskPublisher(for: url)
}
useContext
func useContext<T>(_ context: Context<T>.Type) -> T

A hook to use current context value that is provided by Context<T>.Provider.
The purpose is identical to use Context<T>.Consumer.
See Context section for more details.

let value = useContext(Context<Int>.self)  // Int

See also: React Hooks API Reference


Rules of Hooks

In order to take advantage of the wonderful interface of Hooks, the same rules that React hooks has must also be followed by SwiftUI Hooks.

[Disclaimer]: These rules are not technical constraints specific to SwiftUI Hooks, but are necessary based on the design of the Hooks itself. You can see here to know more about the rules defined for React Hooks.

* In -Onone builds, if a violation against this rules is detected, it asserts by an internal sanity check to help the developer notice the mistake in the use of hooks. However, hooks also has disableHooksRulesAssertion modifier in case you want to disable the assertions.

Only Call Hooks at the Function Top Level

Do not call Hooks inside conditions or loops. The order in which hook is called is important since Hooks uses LinkedList to keep track of its state.

🟒 DO

@ViewBuilder
func counterButton() -> some View {
    let count = useState(0)  // 🟒 Uses hook at the top level

    Button("You clicked \(count.wrappedValue) times") {
        count.wrappedValue += 1
    }
}

πŸ”΄ DON'T

@ViewBuilder
func counterButton() -> some View {
    if condition {
        let count = useState(0)  // πŸ”΄ Uses hook inside condition.

        Button("You clicked \(count.wrappedValue) times") {
            count.wrappedValue += 1
        }
    }
}

Only Call Hooks from HookScope or HookView.hookBody

In order to preserve the state, hooks must be called inside a HookScope.
A view that conforms to the HookView protocol will automatically be enclosed in a HookScope.

🟒 DO

struct CounterButton: HookView {  // 🟒 `HookView` is used.
    var hookBody: some View {
        let count = useState(0)

        Button("You clicked \(count.wrappedValue) times") {
            count.wrappedValue += 1
        }
    }
}
func counterButton() -> some View {
    HookScope {  // 🟒 `HookScope` is used.
        let count = useState(0)

        Button("You clicked \(count.wrappedValue) times") {
            count.wrappedValue += 1
        }
    }
}
struct ContentView: HookView {
    var hookBody: some View {
        counterButton()
    }

    // 🟒 Called from `HookView.hookBody` or `HookScope`.
    @ViewBuilder
    var counterButton: some View {
        let count = useState(0)

        Button("You clicked \(count.wrappedValue) times") {
            count.wrappedValue += 1
        }
    }
}

πŸ”΄ DON'T

// πŸ”΄ Neither `HookScope` nor `HookView` is used, and is not called from them.
@ViewBuilder
func counterButton() -> some View {
    let count = useState(0)

    Button("You clicked \(count.wrappedValue) times") {
        count.wrappedValue += 1
    }
}

See also: Rules of React Hooks


Building Your Own Hooks

Building your own hooks lets you extract stateful logic into reusable functions.
Hooks are composable since they serve as a stateful functions. So, they can be able to compose with other hooks to create your own custom hook.

In the following example, the most basic useState and useEffect are used to make a function provides a current Date with the specified interval. If the specified interval is changed, Timer.invalidate() will be called and then a new timer will be activated.
Like this, the stateful logic can be extracted out as a function using Hooks.

func useTimer(interval: TimeInterval) -> Date {
    let time = useState(Date())

    useEffect(.preserved(by: interval)) {
        let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) {
            time.wrappedValue = $0.fireDate
        }

        return {
            timer.invalidate()
        }
    }

    return time.wrappedValue
}

Let's refactor the Example view at the beginning of the README using this custom hook.

struct Example: HookView {
    var hookBody: some View {
        let time = useTimer(interval: 1)

        Text("Now: \(time)")
    }
}

It's so much easier to read and less codes!
Of course the stateful custom hook can be called by arbitrary views.

See also: Building Your Own React Hooks


How to Test Your Custom Hooks

So far, we have explained that hooks should be called within HookScope or HookView. Then, how can the custom hook you have created be tested?
To making unit testing of your custom hooks easy, SwiftUI Hooks provides a simple and complete test utility library.

HookTester enables unit testing independent of UI of custom hooks by simulating the behavior on the view of a given hook and managing the result values.

Example:

// Your custom hook.
func useCounter() -> (count: Int, increment: () -> Void) {
    let count = useState(0)

    func increment() {
        count.wrappedValue += 1
    }

    return (count: count.wrappedValue, increment: increment)
}
let tester = HookTester {
    useCounter()
}

XCTAssertEqual(tester.value.count, 0)

tester.value.increment()

XCTAssertEqual(tester.value.count, 1)

tester.update()  // Simulates view's update.

XCTAssertEqual(tester.value.count, 1)

Context

React has a way to pass data through the component tree without having to pass it down manually, it's called Context.
Similarly, SwiftUI has EnvironmentValues to achieve the same, but defining a custom environment value is a bit of a pain, so SwiftUI Hooks provides Context API that a more user-friendly.
This is a simple wrapper around the EnvironmentValues.

typealias ColorSchemeContext = Context<Binding<ColorScheme>>

struct ContentView: HookView {
    var hookBody: some View {
        let colorScheme = useState(ColorScheme.light)

        ColorSchemeContext.Provider(value: colorScheme) {
            darkModeButton
                .background(Color(.systemBackground))
                .colorScheme(colorScheme.wrappedValue)
        }
    }

    var darkModeButton: some View {
        ColorSchemeContext.Consumer { colorScheme in
            Button("Use dark mode") {
                colorScheme.wrappedValue = .dark
            }
        }
    }
}

And of course, there is a useContext hook that can be used instead of Context.Consumer to retrieve the provided value.

@ViewBuilder
var darkModeButton: some View {
    let colorScheme = useContext(ColorSchemeContext.self)

    Button("Use dark mode") {
        colorScheme.wrappedValue = .dark
    }
}

See also: React Context


Requirements

  • Swift 5.3+
  • Xcode 12.4.0+
  • iOS 13.0+
  • macOS 10.15+
  • tvOS 13.0+
  • watchOS 6.0+

Installation

Swift Package Manager for Apple Platforms

Xcode menu File > Swift Packages > Add Package Dependency.

Repository: https://github.com/ra1028/SwiftUI-Hooks

Acknowledgements


License

MIT Β© Ryo Aoyama


Comments
  • [Feat Request]: Add support for lazy creation of initial state in useState hook

    [Feat Request]: Add support for lazy creation of initial state in useState hook

    Checklist

    • [X] Reviewed the README and documentation.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    Description

    The signature of useState hook in v0.0.7 looks like this:

    func useState<State>(_ : State) -> Binding<State>
    

    This works fine for basic usage (eg. useState(0) or useState(myConstant)), but might cause a decrease in app performance when used with "factory" method:

    useState(generateMyInitialState())
    

    Although the generateMyInitialState function should not be performance heavy not even for a single call (it runs on main thread), there might be use-cases where a single call to performance heavier "factory method" is acceptable sacrifice.

    The React implementation offers additional overload of the useState hook allowing user to pass a factory method for initial state, rather than initial state itself. A swift alternative could be translated to:

    func useState<State>(_ initialState: @escaping () -> State) -> Binding<State>
    

    and used as:

    useState({ generateMyInitialState() })
    
    // -- OR --
    
    useState(generateMyInitialState)
    

    I would like open a discussion about implementing such feature.

    Example Use Case

    Consider reading a database value used as an initial state for a view. A developer might be tempted to write code like this:

    HookScope {
      let database = useDatabase()
      let myEntity = useState(database.fetchEntity())
      ...
    }
    

    This might work flawlessly for a small project, but easily becomes problematic when the project grows (eg. deep navigation stack, modals, ...).

    To mitigate the problem, user has to explicitly memoize the generated initial state:

    HookScop {
      let database = useDatabase()
      let initialState = useMemo(.once) {
        database.fetchEntity()
      }
      let myEntity = useState(initialState)
      ...
    }
    

    This is a simple workaround/fix, but it's easy to forget about. More over, it might be counter-intuitive for newcomers (considering the useState will only use the generated state once, but generates it on each view render).

    Alternative Solution

    As a solution, I'd like to introduce new API, which should address both intuitiveness and performance of the state hook.

    Option 1

    The simplest solution is to introduce new useState overload, which would take "factory method" as a parameter and utilise useMemo hook internally:

    func useState<State>(_ initialState: @escaping () -> State) -> Binding<State> {
      let state = useMemo(.once) { initialState() }
    
      return useState(state)
    }
    
    // Usage:
    useState(generateMyInitialState())    // Without optimisation (Same behavior as v0.0.7)
    useState { generateMyInitialState() } // With optimisation (New API)
    useState(generateMyInitialState)      // With optimisation (New API - Short syntax)
    

    Pros

    • Source-compatible with existing code
    • Gives user ability to choose between optimised and unoptimised overload

    Cons

    • Introduces dependency on useMemo hook
    • Optimisation is not first class citizen of the hook (StateHook class)
    • Requires the user to know the difference between overloads (unintuitive)

    Option 2

    Replace existing useState hook with new signature:

    func useState<State>(_ initialState: @escaping () -> State) -> Binding<State>
    
    // Usage
    
    useState { generateMyInitialState() } // Optimised - New API
    useState(generateMyInitialState)      // Optimised - New API with short syntax
    

    Update StateHook to support lazy state initialisation:

    private struct StateHook<State>: Hook {
    -   let initialState: State
    +   let initialState: () -> State
        ...
        func makeState() -> Ref {
    -       Ref(initialState: initialState)
    +       Ref(initialState: initialState())
        }
        ...
    }
    

    Optionally, support backwards compatibility by creating useState overload accepting state constant:

    func useState<State>(_ initialState: State) -> Binding<State> {
        return useState({ initialState }) // Fallbacks to implementation above
    }
    

    Pros

    • (optionally) Source-compatible with existing code
    • Gives user ability to choose between optimised and unoptimised overload
    • Optimisation is first class citizen of the hook (StateHook class)

    Cons

    • Requires the user to know the difference between overloads (unintuitive)

    Option 3

    Builds on top of StateHook changes from Option 2, but replaces the closure with @autoclosure argument:

    func useState<State>(_ initialState: @escaping @autoclosure () -> State) -> Binding<State>
    
    // Usage
    
    useState(0)                        // Old API (No performance impact for constants)
    useState(generateMyInitialState()) // New API (Optimised to single call of `generateMyInitialState`)
    

    This allows user to use existing syntax, but leverage improved performance for free.

    Pros

    • Source-compatible with existing code, with performance improvements applied automatically
    • Optimisation is first class citizen of the hook (StateHook class)
    • Uses Swift-native feature to achieve laziness with compact syntax

    Cons

    • Some edge-cases might behave differently than in v0.0.7 (see below)
    • Does not match React syntax
    Incompatible edge cases

    Consider following code, using a side effect while generating initial state:

    var numberOfCalls = 0
    
    func generateState() -> String {
        numberOfCalls += 1
        return ""
    }
    
    func App() -> some View {
        HookScope {
            let state = useState(generateState())
            ...
        }
    }
    

    When re-rendering the view 3 times, the value of numberOfCalls will differ between v0.0.7 (4 - initial + 3 re-renders) and new API (1 - initial). This might cause an issue in theory, but I consider it highly unlikely, considering there is other API for running side effects.

    In addition, the user is able to replicate v0.0.7 behavior with new API like this:

    - let state = useState(generateState())
    + let generatedState = generateState()
    + let state = useState(generatedState)
    

    Proposed Solution

    Considering all the options above, I would vote for implementing Option 3, as it offers nice & compact syntax and automatically applies optimisation to existing code.

    Motivation & Context

    No response

    enhancement 
    opened by josefdolezal 4
  • [Bug]:  The package product 'Hooks' cannot be used as a dependency of this target because it uses unsafe build flags.

    [Bug]: The package product 'Hooks' cannot be used as a dependency of this target because it uses unsafe build flags.

    Checklist

    • [X] This is not a bug caused by platform.
    • [X] Reviewed the README and documentation.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    What happened?

    This error is reported after importing in Xcode

    Expected Behavior

    This package uses unSafeFlag

    Reproduction Steps

    1. xcode import this package
    2. build

    Swift Version

    v5

    Library Version

    lastest

    Platform

    No response

    Scrrenshot/Video/Gif

    No response

    bug 
    opened by GodL 3
  • Cleanup after an effect before executing a new one

    Cleanup after an effect before executing a new one

    Pull Request Type

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Issue for this PR

    Link: N/A

    Description

    Fixes issue where effects with dependencies (eg .preserved(by:)) would cleanup itself after new effect ran. This is different behavior from React's implementation and could cause unexpected results.

    Motivation and Context

    Running the following code with Hooks v0.6.0, after tapping the button, useHook would first run new effect, then cleanup after previous effect. The timeline is:

    1. [Key: 1]
    2. Run Effect [1]
    3. Increment key [Key: 2]
    4. Run Effect [2]
    5. Cleanup Effect [1]

    Steps 4 and 5 should run in opposite order.

    The issue emerges in UseEffectHook.State in willSet property observer. The observation triggers old effect's cleanup by oldValue?() by which time new effect already finished to return new cleanup closure.

    SwiftUI Code
    func App() -> some View {
        HookScope {
            let state = useState(0)
            let _ = useEffect(.preserved(by: state.wrappedValue)) {
                print("subscribe \(state.wrappedValue)")
                return {
                    print("unsubscribe \(state.wrappedValue)")
                }
            }
    
            Text(state.wrappedValue.description)
            Button("Add") {
                state.wrappedValue += 1
            }
        }
    }
    
    React Code
    function App() {
      const [state, setState] = React.useState(0);
    
      useEffect(() => {
        console.log("subscribe " + state);
    
        return () => console.log("unsubscribe " + state);
      }, [state]);
    
      return (
        <div>
          <p>{state}</p>
          <button onClick={() => setState((old) => old + 1)}>Add</button>
        </div>
      );
    }
    

    Running the sample code in patch-1 branch, the results are same as React app above.

    Impact on Existing Code

    The change is source-compatible.

    Screenshot/Video/Gif

    opened by josefdolezal 1
  • Lazy initial state using closures in useState hook

    Lazy initial state using closures in useState hook

    Pull Request Type

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Issue for this PR

    Link: #27

    Description

    Implements feature request #27 (closures for initial state in useState). The solution implements Option 2 from the linked issue.

    Looking for an overall feedback on the updated documentation πŸ™.

    Motivation and Context

    Adds support for instantiating initial state in useState hook. This might help with app performance in some specific cases. Described into detail in linked issue.

    Impact on Existing Code

    Source compatible up-to edge cases (see discussion in #27).

    Screenshot/Video/Gif

    N/A

    opened by josefdolezal 0
  • Enable animations in state hook

    Enable animations in state hook

    Pull Request Type

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Issue for this PR

    Link: N/A

    Description

    This PR fixes a bug occurring when a user is trying to animate useState changes using .animation() modifier.

    I could not find any way to test the code correctly or at least to create failing test - any tips on that?

    Motivation and Context

    Given following code:

    let myState = useState(..).animation(.default)
    ...
    ...
    myState.wrappedValue = ...
    
    • Expected behavior: Views will animate transition between state changes
    • Actual: View re-renders itself on state change, but .default animation is ignored and the transition is immediate.
    Demo app source code
    struct App: View {
        @State private var animate: Bool = true
        @State private var isRounded: Bool = true
    
        var body: some View {
            Form {
                Section {
                    Toggle("Animate changes", isOn: $animate)
                }
    
                SwiftUIStateDemo(animateChanges: animate)
    
                HooksStateDemo(animateChanges: animate)
            }
        }
    }
    
    struct SwiftUIStateDemo: View {
        var animateChanges: Bool
        @State private var isRounded: Bool = false
    
        var body: some View {
            Section("Plain SwiftUI State") {
                Preview(isRounded: $isRounded.animation(animateChanges ? .default : nil))
            }
        }
    }
    
    func HooksStateDemo(animateChanges: Bool) -> some View {
        HookScope {
            let isRounded = useState(false).animation(animateChanges ? .default : nil)
    
            Section("Hooks State") {
                Preview(isRounded: isRounded.animation(animateChanges ? .default : nil))
            }
        }
    }
    
    func Preview(isRounded: Binding<Bool>) -> some View {
        HStack {
            Button("Change", action: { isRounded.wrappedValue.toggle() })
                .buttonStyle(.bordered)
    
            Spacer()
    
            Color.red
                .frame(width: 50, height: 50)
                .cornerRadius(isRounded.wrappedValue ? 25 : 0)
        }
    }
    

    Impact on Existing Code

    This change is source-compatible, the fix will only affect users if they use .animation() modifier on state binding.

    Screenshot/Video/Gif

    | main branch | Pull Request | |-----|-----| |

    opened by josefdolezal 0
  • fix: Remove unsafe flags from the package definition

    fix: Remove unsafe flags from the package definition

    Pull Request Type

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Issue for this PR

    Link: https://github.com/ra1028/swiftui-hooks/issues/23

    Description

    The Package.swift file contained unsafeFlags to check actor data race with runtime warnings, but it was blocking to use the package as a dependency in an external project. The PR just removes them.

    Impact on Existing Code

    Nothing

    opened by ra1028 0
  • Remove EXCLUDED_ARCHS settings from example project

    Remove EXCLUDED_ARCHS settings from example project

    Pull Request Type

    • [ ] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [x] Chore

    Description

    It seems that the setting is no longer needed to run example apps in M1 mac.

    opened by ra1028 0
  • Introduce HookUpdateStrategy instead of HookComputation

    Introduce HookUpdateStrategy instead of HookComputation

    Breaking changes

    • Introduce HookUpdateStrategy instead of HookComputation. HookUpdateStrategy doesn't have .always but now nil can be used instead. nil is a default value for useEffect.
    • Rename Hook.makeValue(coordinator:) to Hook.value(coordinator:).
    • Rename Hook.compute(coordinator:) to Hook.updateState(coordinator:).
    • Rename Hook.shouldDeferredCompute to Hook.shouldDeferredUpdate.
    opened by ra1028 0
  • refactor: Improve example apps

    refactor: Improve example apps

    • Improve example apps.
    • Add an example of unit-testing with TheMovieDB app.
    • Merge Hooks and HooksTesting modules into a single module to resolve the linking issue.
    opened by ra1028 0
  • Rename AsyncStatus to AsyncPhase

    Rename AsyncStatus to AsyncPhase

    Breaking change

    AsyncImagePhase has been added on iOS15 and it's quite similar to AsyncStatus so it would make more sense if they have a similar name.

    opened by ra1028 0
  • useEffect seems to call dispose too late

    useEffect seems to call dispose too late

    Example

    struct ExampleView : HookView {
      var hookBody: some View {
        useEffect(.once) {
          print("useeffect create")
          return {
            print("useeffect dispose")
          }
        }
        return Text("")
          .onAppear() {
            print("swiftui create")
          } 
          .onDisappear() {
            print("swiftui dispose")
          }
    }
    

    Outputs the following

    when view is created:

    • useeffect create
    • swiftui create

    when view is disappears:

    • swiftui dispose

    when view is recreated

    • useeffect create
    • useeffect dispose
    • swiftui create

    Expected

    useEffect's dispose to be called when the view disappears

    opened by kyunghoon 0
  • [Bug]: HookScope invalidates view on each Environment change

    [Bug]: HookScope invalidates view on each Environment change

    Checklist

    • [X] This is not a bug caused by platform.
    • [X] Reviewed the README and documentation.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    What happened?

    Consider a simple SwiftUI app:

    @main
    struct _App: App {
        var body: some Scene {
            WindowGroup {
                __App__
            }
        }
    }
    

    where __App__ is a placeholder for either plain SwiftUI App or its Hooks alternative.

    SwiftUI App
    struct SwiftUIApp: View {
        var body: some View {
            let _ = print("SwiftUI - I changed")
            Text("")
        }
    }
    
    Hooks App
    func HooksApp() -> some View {
        HookScope {
            let _ = print("HookScope - I changed")
            Text("")
        }
    }
    

    When you run both apps from Xcode 13.4.1 on iOS 15.5, you will see the following output in the console:

    | SwiftUIApp() | HooksApp() | | --- | --- | | SwiftUI - I changed
    Β 
    Β  | HookScope - I changed
    HookScope - I changed
    HookScope - I changed |

    Considering both apps are stateless and do not invalidate its body, the console result should match. When tracking down the issue using SwiftUI's _printChange(), it's clear that it's caused by library's HookScopeBody struct:

    private struct HookScopeBody<Content: View>: View {
        @Environment(\.self) private var environment
        ...
        var body: some View {
            if #available(iOS 15.0, *) {
    +       let _ = Self._printChanges()
            dispatcher.scoped(environment: environment, content)
        }
    }
    

    Changes printed by SwiftUI are:

    HookScopeBody: @self, @identity, _dispatcher, _environment changed.
    HookScopeBody: _environment changed.
    HookScopeBody: _environment changed.
    

    The problem is in observing Environment(\.self) which invalidates HookScopeBody's body for each change in environment, causing a view to re-render.

    I haven't dig into finding a solution yet, but wanted to keep the issue on sight as it can potentially cause unexpected re-renders of the whole app.

    Notes:

    • ⚠️ HookView is also affected (as it internally uses HookScope)
    • ⚠️ When working on a solution, we need to keep in mind that Context internally uses environments too.

    Expected Behavior

    HookScope body should only invalidate for key path specified in useEnvironment or types used in useContext.

    Reproduction Steps

    1. Create a new project and replace @main struct with _App struct from above
    2. Replace the __App__ with SwiftUIApp() and run
    3. Replace SwiftUIApp() call with HooksApp()
    4. Compare console outputs

    Swift Version

    5.6+

    Library Version

    <= 0.0.8

    Platform

    No response

    Scrrenshot/Video/Gif

    No response

    bug 
    opened by josefdolezal 4
Releases(0.0.8)
  • 0.0.8(Jul 3, 2022)

    What's Changed

    • Enable animations in state hook by @josefdolezal in https://github.com/ra1028/swiftui-hooks/pull/26
    • Lazy initial state using closures in useState hook by @josefdolezal in https://github.com/ra1028/swiftui-hooks/pull/28

    Full Changelog: https://github.com/ra1028/swiftui-hooks/compare/0.0.7...0.0.8

    Source code(tar.gz)
    Source code(zip)
  • 0.0.7(Jun 24, 2022)

    What's Changed

    • Cleanup after an effect before executing a new one by @josefdolezal in https://github.com/ra1028/swiftui-hooks/pull/25

    New Contributors

    • @josefdolezal made their first contribution in https://github.com/ra1028/swiftui-hooks/pull/25

    Full Changelog: https://github.com/ra1028/swiftui-hooks/compare/0.0.6...0.0.7

    Source code(tar.gz)
    Source code(zip)
  • 0.0.6(May 9, 2022)

    What's Changed

    • Remove EXCLUDED_ARCHS settings from example project by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/22
    • fix: Remove unsafe flags from the package definition by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/24

    Full Changelog: https://github.com/ra1028/swiftui-hooks/compare/0.0.5...0.0.6

    Source code(tar.gz)
    Source code(zip)
  • 0.0.5(Apr 17, 2022)

    What's Changed

    • feat: Add useAsync and useAsyncPerform hooks using Concurrency by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/4

    Full Changelog: https://github.com/ra1028/swiftui-hooks/compare/0.0.4...0.0.5

    Source code(tar.gz)
    Source code(zip)
  • 0.0.4(Apr 12, 2022)

    What's Changed

    • chore: Gardening by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/13
    • Introduce HookUpdateStrategy instead of HookComputation by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/14
    • chore: Fix lint warnings by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/15
    • chore: Update CI config to run on macOS Big Sur by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/16
    • feat: Add environment value that to disable assertions by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/17
    • Support for Swift 5.6 and drop support for lower versions by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/19
    • Add GitHub community files and Documentation by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/20
    • chore: Replace all upper case repo/library names with lower case letter by @ra1028 in https://github.com/ra1028/swiftui-hooks/pull/21

    Full Changelog: https://github.com/ra1028/swiftui-hooks/compare/0.0.3...0.0.4

    Source code(tar.gz)
    Source code(zip)
Owner
Ryo Aoyama
β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–€β–„β–‘β–‘β–‘β–„β–€β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–„β–ˆβ–€β–ˆβ–ˆβ–ˆβ–€β–ˆβ–„β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–ˆβ–€β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–€β–ˆβ–‘β–‘β–‘ β–‘β–‘β–‘β–ˆβ–‘β–ˆβ–€β–€β–€β–€β–€β–€β–€β–ˆβ–‘β–ˆβ–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–€β–€β–‘β–‘β–‘β–€β–€β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘
Ryo Aoyama
Betcalsa trims, enhances, and makes documents readable.

Betcalsa Betcalsa trims, enhances, and makes documents readable. Automatically detect your document Easily edit with your scans after you have scanned

Emre Durukan 56 Dec 11, 2022
A weather app developed in React Native. It is the React Native version of SwiftWeather.

ReactNativeWeather A weather app developed in React Native. It is the React Native version of SwiftWeather How to run the app Install react-native If

Jake Lin 22 Jun 7, 2022
Visualize your dividend growth. DivRise tracks dividend prices of your stocks, gives you in-depth information about dividend paying stocks like the next dividend date and allows you to log your monthly dividend income.

DivRise DivRise is an iOS app written in Pure SwiftUI that tracks dividend prices of your stocks, gives you in-depth information about dividend paying

Kevin Li 78 Oct 17, 2022
An App that gives a nice interface where the user can type in their start location and destination

SixtCarSummoner What it does We developed an App that gives a nice interface where the user can type in their start location and destination. The user

Dominik Schiwietz 1 Nov 21, 2021
Recipes app written in SwiftUI using Single State Container

swiftui-recipes-app Recipes app is written in SwiftUI using Single State Container This app implemented as an example of a Single State Container conc

Majid Jabrayilov 512 Dec 31, 2022
GroceryMartApp-iOS-practice - To Practice fundamental SwiftUI feature like navigation, state mamagement, customazing etc

?? GroceryMartApp-iOS-practice μ•„λž˜μ˜ λ‚΄μš©μ€ μŠ€μœ—ν•œ SwiftUI μ±…μ˜ μ‹€μ „ μ•± κ΅¬ν˜„ν•˜κΈ° 을 λ°”νƒ•μœΌλ‘œ μ •λ¦¬ν•œ λ‚΄μš©μž…λ‹ˆλ‹€

Jacob Ko 0 Jan 7, 2022
This is a mastodon sample SwiftUI app implemented with the architecture of state management with normalized cache.

MastodonNormalizedCacheSample This is a mastodon sample SwiftUI app. This app is implemented with the architecture of state management with Normalized

null 5 Nov 27, 2022
Learn how to structure your iOS App with declarative state changes using Point-Free's The Composable Architecture (TCA) library.

Learn how to structure your iOS App with declarative state changes using Point-Free's The Composable Architecture (TCA) library.

Tiago Henriques 0 Oct 2, 2022
IOS Dracker Payment - An iOS and React app to send/receive money and manage debt

An iOS and React app to send/receive money and manage debt. This app allows users to create transactions, add descriptions, tag images, tag notes, and manage them.

Dharmendra solanki 0 Jan 30, 2022
An experiment to use Firebase and React Native to build a wwdc.family app

wwdc.family This is an experiment to use Firebase and React Native to build a wwdc.family app. Don't use that source code as reference - I have no pri

WWDC Family 190 Feb 9, 2022
SwiftUI & Combine app using MovieDB API. With a custom Flux (Redux) implementation.

MovieSwiftUI MovieSwiftUI is an application that uses the MovieDB API and is built with SwiftUI. It demos some SwiftUI (& Combine) concepts. The goal

Thomas Ricouard 6.2k Jan 8, 2023
SwiftWebUI - A demo implementation of SwiftUI for the Web

SwiftWebUI More details can be found on the related blog post at the Always Right Institute. At WWDC 2019 Apple announced SwiftUI. A single "cross pla

SwiftWebUI 3.8k Dec 28, 2022
SwiftUI implementation of Conway’s Game of Life β€” also known as β€œLife”.

Life Conway’s Game of Life SwiftUI implementation of Conway’s Game of Life β€” also known simply as β€œLife”. About I’m Martin, an indie dev from Berlin.

Martin Lexow 23 Jan 21, 2022
SwiftUI implementation of xcodes by RobotsAndPencils

XcodeUpdates SwiftUI implementation of xcodes by RobotsAndPencils Screenshots Technical Details Project supports macOS Big Sur (11.+) Project is writt

Ruslan Alikhamov 222 Oct 2, 2022
Tictactoe-ultimatum - iOS implementation of Ultimate Tic-Tac-Toe game

TicTacToe Ultimatum An iOS app in Swift implementing the classic game of Ultimat

Max Khrapov 1 Jan 21, 2022
The Discord API implementation behind Swiftcord, implemented completely from scratch in Swift

DiscordKit The Discord API implementation that powers Swiftcord This implementation has fully functional REST and Gateway support, but is mainly geare

Swiftcord 71 Dec 20, 2022
Swift implementation of the elm architecture (TEA)

Swiftea If you were looking for a something like this: TEA (The Elm Architecture) MVU (Model-View-Update) MVI (Model-View-Intent) Redux-like Flux-like

Dmitrii Cooler 10 Aug 30, 2022
Vector editor to showcase advanced scroll view and SwiftUI

ShapeEdit ShapeEdit is a showcase for Advanced ScrollView, inspired by WWDC sample with the same name. ShapeEdit is build in SwiftUI, with exception o

Dmytro Anokhin 34 Dec 29, 2022
A simple and lightweight Swift package which provides a SwiftUI view for interactive geo coordinates input!

LocationPicker for SwiftUI LocationPicker for SwiftUI is a very simple and lightweight Swift package which provides you a SwiftUI view for interactive

Alessio Rubicini 17 Dec 7, 2022