Giter VIP home page Giter VIP logo

Comments (5)

VasilyKrainov avatar VasilyKrainov commented on June 13, 2024 1

Thank you so much again for your so detailed explanation of possible ways to deal with my unusual cases. Now I have answers for all my questions, and the issue can be closed.

from swiftrex.

luizmb avatar luizmb commented on June 13, 2024

Hi!

In SwiftRex currently there's no options to dynamically add or remove things from the store. I wrote this middleware some time ago (https://github.com/SwiftRex/GatedMiddleware) to explore the option to disable or enable middlewares dynamically, either by receiving an action or after certain state matched certain condition. I used to disable analytics, or disable debuggers for certain builds, but to be very honest I think there are better ways of doing that. The Middleware should know what's relevant for itself and ignore all the rest. Same for reducers.

Big part of ignoring irrelevant actions comes from the lifting, as you well mentioned. Let's say you have 2 forms written in 2 different modules (SPM packages, for example) and a main module that groups them all. Because this 2 forms are completely different business logic, very isolated, your Action (and also State) structure will also be different branches in your tree, like this:

// Main Target
import Form1
import Form2
enum AppAction {
  case .form1(Form1Action)
  case .form2(Form2Action)
}

// Form1 Framework
enum Form1Action {
  case list, delete, insert, update
}

// Form2 Framework
enum Form2Action {
  case list, delete, insert, update
}

Your middlewares + reducers will focus on FormNAction and ignore anything else. In fact, making them generic over FormNAction will force you to lift them to AppAction (using the \AppAction.form2 prism). The lift won't forward form1 actions to form2, or vice-versa, even because these modules don't even know the other action.

// Form1 Framework
class SomeMiddleware: MiddlewareBase<Form1Action, Form1Action, Form1State> { ... }

// Main Target
let mainMiddleware = Form1.SomeMiddleware()
  .lift(
    inputAction: \AppAction.form1,
    outputAction: AppAction.form1,
    state: \AppState.form1
  )

Once your store receives a form2 action, the lift for input action will try to extract the branch \AppAction.form1 out of it, which returns nil (thanks to the prism getter), so the action will never be forwarded to the Form1.SomeMiddleware.


When the modules share some business logic, you will have to ignore actions using business logic, there's no other way. The GatedMiddleware was a generic attempt to do that, but you don't actually need it. Let's say, your OnboardingMiddleware will handle all sorts of actions and it's only relevant while the user is performing the Onboarding on your app for the first time. After that, you want it to ignore actions.

Well... In that case, somewhere in your app you will need a piece of state .isOnboardingFinished that will be set by the OnboardingReducer (and maybe even persisted to UserDefaults/CoreData/whatever by your OnboardingMiddleware, and restored every app launch). In your Middleware's func handle(action: AppAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer), the first thing you should do is to read getState().somewhere.isOnboardingFinished and in case you receive true, you bypass that middleware.

The cost is very very low.

Form1Middleware and Form2Middleware are never destroyed. You don't have to. If you run processes that you want to cleanup, for example a timer that fetches certain data from the web, or a websocket connection, or whatever, and you want this at some point to be cancelled, you must receive an action like ".moduleForm1IsFinished" and act on that, usually cleaning up your cancellables (self.cancellables = Set() / self.disposeBag.dispose()) which will destroy every process ran by the middleware. That could also be reduced in the state so whenever moduleForm1IsFinished action arrives, you change certain flag in your state and Form1 Middleware will read this before any task to ensure it's still alive.

I don't see a use case for when you really want to remove the middleware from the store. But if this is what you really want, you can have a middleware container that is dynamic, and eventually sets its internal middleware to nil.

Imagine this:
https://github.com/SwiftRex/SwiftRex/blob/develop/Sources/SwiftRex/Middlewares/LiftMiddleware.swift#L16

where part middleware can be set to nil. It's possible, I just honestly think it's not the best way because it requires a lot of logic that can fail. IMHO it's better to simply ignore actions and logically cancel subscriptions.

Please tell me if I answered your question or there are cases that I don't see here.

from swiftrex.

VasilyKrainov avatar VasilyKrainov commented on June 13, 2024

Thank you so much for such detailed explanation!
You are suggesting good way to work with apps where all modules (view + data) are different.
But what can you advice for a situation when one module may have several instances? For example, AppModule shows FormModule, and then FormModule can create another instance of FormModule with different data, and so on.

Let's imagine that SwiftRex has possibility to add/remove reducers/middlewares on-fly. Then I can have such structures:

struct AppState {
  someInfo: String,
  form: FormState?
}
class FormState {
  field1: Int,
  field2: String,
  form: FormState?
}
  1. When the app is launched, it creates the global store (using hypotetic DSL):
    store = store(app.reducer, app.middleware)
  2. When the app creates and shows FormModule N1 it does something like this:
store.reducer.append(form1.reducer.liftedToApp)
store.middleware.append(form1.middleware.liftedToApp)
  1. And now, when Form N1 creates and shows Form N2:
store.reducer.append(form2.reducer.liftedToParentForm)
store.middleware.append(form2.middleware.liftedToParentForm)

In both 2) and 3) steps Action and State can be easily converted from/to parent ones.
Removing reducers/middlewares on module destruction also works well in this fantasy.

But SwiftRex doesn't support possibility to add/remove reducers/middlewares on-fly. In this situation, when Form sends an Action, how reducer/middleware could recoginize which Form instance sends it - N1 or N2? I see following decision:

struct AppState {
  someInfo: String,
  forms: [String : FormState]
}
struct FormState {
  field1: Int,
  field2: String
}
enum FormAction {
  case tapEdit(String)
}

AppState should track every Form instance by some identifier (String dictionary key), and every FormAction should contain the identifier of the Form that sends it, so reducer/middleware can extract correct FormState.
Are there more elegant ways exist to deal with such situations?

from swiftrex.

luizmb avatar luizmb commented on June 13, 2024

TL;DR:
You AppState doesn't need the dictionary or array of Forms, only the entry-point (root) of your tree. With identifiers you should be able to traverse the tree from the middleware/reducer directly or using lift.


Down the rabbit hole:

So, I wouldn't indeed use nested Middlewares because of nested state. Or reducers. If Form1 has a Form1 as child, you're still in the realm of Form1 business and imho the Form1Middleware has the logic to deal with Form1 knowing that it may have a child of type Form1 as well.
I have similar recursive state in one of my apps. In that case, we have a content browser, so imagine you are browsing Spotify, then you tap Rock, then 60s, then Beatles, then the White Album and then finally you tap a song that plays in your speakers. This structure is nothing but a content tree, which I have a recursive state. For me, the perfect type to represent trees are enums. So imagine an enum like:

public enum ContentBrowsingTree: Hashable {
    case root(contentID: String)
    indirect case node(contentID: String, parent: ContentBrowsingTree)

    /// Prints the content ID for the current node in the tree
    public var nodeId: String {
        switch self {
        case let .node(nodeId, _): return nodeId
        case let .root(rootId): return rootId
        }
    }

    /// Given a content ID, finds exactly the node where this content object is present in this tree
    /// or nil in case the contentID is not part of this tree.
    /// - Parameter contentID: some content ID to search in this tree
    /// - Returns: the tree, from the root to the content object provided, or nil if the content ID
    ///            is not part of this tree
    public func node(for contentID: String) -> ContentBrowsingTree? {
        switch self {
        case let .node(nodeId, parent):
            if nodeId == contentID { return self }
            return parent.node(for: contentID)
        case let .root(contentID):
            if contentID == contentID { return self }
            return nil
        }
    }

    /// Given a content ID, finds a node with that content object ID, and then its immediate child.
    /// If the content ID is not found, or it's found but it's a leaf node, then nil will be returned.
    /// - Parameter parentId: a content object ID from the parent of the node we are searching for.
    /// - Returns: a child of certain container, if found, or nil if the container is leaf node or the
    ///            ID is not found
    public func child(of parentId: String) -> ContentBrowsingTree? {
        switch self {
        case .root: return nil
        case let .node(_, parent):
            if parent.nodeId == parentId {
                return self
            }
            return parent.child(of: parentId)
        }
    }
}

You need an identifier for each level. The root could be "/" then from that you can build something like "/Genre/Rock/Category/60s/Artist/Beatles/Album/TheWhiteAlbum"

Your reducer and middleware have a way to find precisely the content as if it was a flat structure, but work in a tree structure whenever needed. This is also helpful for views, because in my case I use NavigationLink to show all possible children of certain content container, that view has a view state set to nil when no children is selected, but once a child is selected I use the ContentID (which is hashable) in the Binding for the NavigationLink, which makes the NavigationView to push the child. the Navigation is also recursive, and you can have as many levels as you need. ContentList -> NavigationLink to destination ContentList -> NavigationLink to destination ContentList -> NavigationLink to destination ContentList ... etc etc etc.

My middleware understands Content Browsing, not content browsing for the root directory, or content browsing for album A.

If this was the case, you could also lift using the content ID. Using those helpers in the ContentBrowsingTree you should be able to fetch the immediate child, or the leaf node, the root node, the parent, etc. With that, you could identify your action based on the content tree.

Here you find some ways to use lifting with Identifiable elements in a collection, to lift single element within an array:
https://github.com/SwiftRex/SwiftRex/blob/develop/Sources/CombineRex/EffectMiddleware%2BLiftToCollection.swift#L34
https://github.com/SwiftRex/SwiftRex/blob/develop/Sources/SwiftRex/CoreTypes/Pipeline/Reducer%2BLiftToCollection.swift#L5
For collection of non-Identifiable, through index, it's possible as well. You can use that idea to make a lift that works in a tree structure, if this is what you look for. The View part of that can also use projection with the same purpose, transform a Store Projection that is a collection of elements, into a Store Projection that is one element in that collection:
https://github.com/SwiftRex/CombineRextensions/blob/master/Sources/CombineRextensions/ForEach%2BExtensions.swift#L55

I guess you can use similar idea for trees as well. Every tree can be turned into a flat array as long as you make them identifiable somehow. So if your Form has hierarchy, you need to track somehow where in the Form you are.

One last thing to mention. If you follow the links to the LiftToCollection middleware, you will see that I rely on a special type of action:

public struct ElementIDAction<ID: Hashable, Action> {
    public let id: ID
    public let action: Action

    public init(id: ID, action: Action) {
        self.id = id
        self.action = action
    }
}

This is a way to associate actions of individuals to action of collections.
You could have:

public typealias ContentScopedAction = ElementIDAction<ContentID, ContentItemAction>

You ContentItemAction would be "tapEdit, tapSave, delete" without the need of associated values in these enum cases. Then you scope this enum to a ContentScopedAction containing the proper identifier. Remember:

enum X {
   case a(String)
   case b(String)
   case c(String)
}

// is algebraically the same as:

struct Scope {
    let id: String
    let y: Y
}

enum Y {
    case a, b, c
}

The second is more clean: n * a + n * b + n * c == n * (a + b + c).


Please let me understand better your use case and maybe we can find a good solution for your case.

from swiftrex.

luizmb avatar luizmb commented on June 13, 2024

I'm glad to hear that you've got your answers. But in any case if you find ways to improve the API please let me know and we can reopen the issue and discuss a bit more.

from swiftrex.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.