如何在iOS MVVM中处理子视图中的用户交互

huangapple go评论71阅读模式
英文:

How to handle user interactions in subviews with iOS MVVM

问题

实现MVVM与表视图控制器时,通常会有一个单一的父视图模型和一组子视图模型,每个单元格都有一个。假设每个单元格都有一个"喜欢"按钮,现在用户点击了其中一个"喜欢"按钮。

在查阅Stack Overflow后,我看到了三种可能的处理方法:

  1. 点击操作发送到子视图模型,子视图模型在内部处理"喜欢"操作。

  2. 点击操作发送到子视图模型,子视图模型将意图传递给父视图模型,由父视图模型处理"喜欢"操作。

  3. 点击操作发送到表视图控制器(使用单元格代理或闭包),然后表视图控制器将点击操作传递给父视图模型。

就我个人而言,我更喜欢第二和第三种方法。让我困惑的是,在第三种方法中,子视图模型仅负责输出(呈现数据),而不负责输入(处理交互)。相反,父视图模型负责处理子视图的交互。因为通常一个视图模型会同时处理视图的输入和输出。

哪种方法更好?或者是否有更好的方法来实现相同的目标?
任何建议都将有帮助。

英文:

For implementing MVVM with a table view controller, it is often to have a single parent view model and a bunch of child view models for each cell. Let's say each cell has a like button, and now the user taps one of the like buttons.

After searching through stack overflow, I see three possible ways to handle the flow:

  1. The tapped action is sent to the child view model, the child view model handles the like action internally.

  2. The tapped action is sent to the child view model, the child view models pass the intent to the parent view model, which handles the like action.

  3. The tapped action is sent to the table view controller (using cell delegate or closure), the table view controller then pass the tapped action to the parent view model.

Personally, I prefer the 2nd and 3rd approach. What confuses me is that in the 3rd approach, the child view model is only responsible for the output (presenting data), but not for the input (handling interactions). Instead, the parent view model is responsible for handling the child view's interaction. Because normally a view model will handle both for its view.

Which approach is better? Or are there better ways to achieve the same goal?
Any advise would be helpful.

答案1

得分: 1

在这种情况下,我首先要问自己的第一个问题是,视图控制器中的任何状态是否需要根据单元格中的输入而更改。如果是这样,那么选项2是最佳选择(假设使用RxSwift)。如果您使用委托/闭包而不是可观察对象,则选项3对于减少所需委托的数量很有用。

根据您的描述,似乎在用户点击按钮时不需要更新视图控制器的状态,因此选项1听起来是最好的。

以下是我可能会使用我的CLE架构实现它的方式... 您可以将connect函数视为视图模型。

extension ViewController {
    // 视图控制器没有太多事情要做。只需获取“likables”数组并在表视图上显示它们。
    func connect(api: API) {
        api.response(.fetchLikables)
            .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: LikableCell.self)) { _, item, cell in
                cell.connect(api: api, item: item)
            }
            .disposed(by: disposeBag)

        api.error
            .map { $0.localizedDescription }
            .bind(onNext: presentScene(animated: true) { message in
                UIAlertController(title: "Error", message: message, preferredStyle: .alert).scene {
                    $0.connectOK()
                }
            })
            .disposed(by: disposeBag)
    }
}

extension LikableCell {
    // 单元格具有有趣的行为。如果事物被喜欢,那么“likeButton”将被选中。当点击按钮时,更新状态并发送网络请求。如果请求失败,则重置状态。
    func connect(api: API, item: LikableThing) {
        enum Input {
            case tap
            case updateSucceeded
            case updateFailed
        }
        cycle(
            input: likeButton.rx.tap.map(to: Input.tap),
            initialState: (current: item.isLiked, reset: item.isLiked),
            reduce: { state, input in
                switch input {
                case .tap:
                    state.current.toggle() // 立即切换喜欢状态。
                case .updateSucceeded:
                    state.reset = state.current // 如果服务器成功,更新重置状态
                case .updateFailed:
                    state.current = state.reset // 如果服务器失败,更新当前状态。
                }
            },
            reaction: { args in
                args
                    .filter { $0.1 == .tap } // 仅在用户点击时进行网络请求
                    .flatMapLatest { state, _ in
                        return api.successResponse(.setLikable(id: item.id, isLiked: !state.current))
                            .map { $0 ? Input.updateSucceeded : Input.updateFailed }
                    }
            }
        )
        .map { $0.current }
        .bind(to: likeButton.rx.isSelected)
        .disposed(by: disposeBag)
    }
}

struct LikableThing: Decodable, Identifiable {
    let id: Identifier<Int, LikableThing>
    let isLiked: Bool
}

extension Endpoint where Response == [LikableThing] {
    static let fetchLikables: Endpoint = Endpoint(
        request: apply(URLRequest(url: baseURL)) { request in
            // 配置请求
        },
        decoder: JSONDecoder()
    )
}

extension Endpoint where Response == Void {
    static func setLikable(id: LikableThing.ID, isLiked: Bool) -> Endpoint {
        let request = URLRequest(url: baseURL)
        // 配置请求
        return Endpoint(request: request)
    }
}
英文:

The first question I would ask myself in this situation is whether any state in the view controller has to change based on inputs into the cell. If so, then option 2 is the best choice (assuming RxSwift.) If you are using delegates/closures instead of Observables then option 3 is handy to reduce the number of delegates required.

From your description, it doesn't sound like the view controller's state needs to be updated when the user taps the button, so option 1 sounds like the best.

Here is how I would likely implement it using my CLE architecture... You can think of the connect functions as view models.

extension ViewController {
// The view controller doesn&#39;t have much to do. Just fetch the array of
// likables and show them on the table view.
func connect(api: API) {
api.response(.fetchLikables)
.bind(to: tableView.rx.items(cellIdentifier: &quot;Cell&quot;, cellType: LikableCell.self)) { _, item, cell in
cell.connect(api: api, item: item)
}
.disposed(by: disposeBag)
api.error
.map { $0.localizedDescription }
.bind(onNext: presentScene(animated: true) { message in
UIAlertController(title: &quot;Error&quot;, message: message, preferredStyle: .alert).scene {
$0.connectOK()
}
})
.disposed(by: disposeBag)
}
}
extension LikableCell {
// The cell has the interesting behavior. If the thing is liked, then the
// `likeButton` is selected. When the button is tapped, update the state
// and send the network request. If the request fails, then reset the state.
func connect(api: API, item: LikableThing) {
enum Input {
case tap
case updateSucceeded
case updateFailed
}
cycle(
input: likeButton.rx.tap.map(to: Input.tap),
initialState: (current: item.isLiked, reset: item.isLiked),
reduce: { state, input in
switch input {
case .tap:
state.current.toggle() // toggle the like state right away.
case .updateSucceeded:
state.reset = state.current // if server success, update the reset
case .updateFailed:
state.current = state.reset // if server fail, update the current state.
}
},
reaction: { args in
args
.filter { $0.1 == .tap } // only make network request when user taps
.flatMapLatest { state, _ in
return api.successResponse(.setLikable(id: item.id, isLiked: !state.current))
.map { $0 ? Input.updateSucceeded : Input.updateFailed }
}
}
)
.map { $0.current }
.bind(to: likeButton.rx.isSelected)
.disposed(by: disposeBag)
}
}
struct LikableThing: Decodable, Identifiable {
let id: Identifier&lt;Int, LikableThing&gt;
let isLiked: Bool
}
extension Endpoint where Response == [LikableThing] {
static let fetchLikables: Endpoint = Endpoint(
request: apply(URLRequest(url: baseURL)) { request in
// configure request
},
decoder: JSONDecoder()
)
}
extension Endpoint where Response == Void {
static func setLikable(id: LikableThing.ID, isLiked: Bool) -&gt; Endpoint {
let request = URLRequest(url: baseURL)
// configure request
return Endpoint(request: request)
}
}

huangapple
  • 本文由 发表于 2023年7月27日 23:44:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76781435.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定