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

How to handle user interactions in subviews with iOS MVVM

提问人:Andy 提问时间:7/27/2023 最后编辑:Andy 更新时间:7/29/2023 访问量:103

问:

为了实现具有表视图控制器的 MVVM,通常需要为每个单元格使用一个父视图模型和一堆子视图模型。假设每个单元格都有一个“赞”按钮,现在用户点击其中一个“赞”按钮。

在搜索堆栈溢出后,我看到了三种处理流的可能方法:

  1. 点击的操作将发送到子视图模型,子视图模型在内部处理类似操作。

  2. 点击的操作将发送到子视图模型,子视图模型将意图传递给父视图模型,父视图模型处理类似操作。

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

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

哪种方法更好?或者有更好的方法来实现同样的目标?
任何建议都会有所帮助。

iOS 操作系统 MVVM 视图模型 RX-SWIFT RX- 可可

评论


答:

1赞 Daniel T. 7/28/2023 #1

在这种情况下,我会问自己的第一个问题是,视图控制器中的任何状态是否必须根据单元格中的输入而更改。如果是这样,那么选项 2 是最佳选择(假设是 RxSwift)。如果您使用的是委托/闭包而不是 Observables,那么选项 3 可以方便地减少所需的委托数量。

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

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

extension ViewController {
    // The view controller doesn'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: "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 {
    // 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<Int, LikableThing>
    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) -> Endpoint {
        let request = URLRequest(url: baseURL)
        // configure request
        return Endpoint(request: request)
    }
}

评论

0赞 Andy 8/7/2023
不错的方法。但是,如何使数据在父视图模型和子视图模型之间保持同步?子视图模型在内部更新帖子,但旧帖子仍存在于父视图模型中(在帖子数组内)。如果我随后更新 post 数组,它将触发父视图生成新的子视图模型,这似乎在子视图模型中更新没有意义。
0赞 Daniel T. 8/8/2023
旧帖子不存在于帖子数组内的父视图模型中,因为父视图模型中没有帖子数组。如果您设置了会导致再次调用的内容,服务器将向您发送正确同步的数据。fetchPosts
0赞 Andy 8/8/2023
明白了。让我感到困惑的是,当服务器将更新的帖子发送回父视图模型时,它会触发父视图模型创建一组新的子视图模型,然后触发表视图更新数据,这会导致单元格更新,但单元格本身已经被单元格视图模型更新了。这种重复的更新似乎没有必要。
1赞 Daniel T. 8/8/2023
服务器不会将更新的帖子发送回父视图模型。仅当视图首次加载时(在 中调用)才会进行调用。您必须在上面添加代码才能再次调用(也许是拉取刷新?如果要添加要素,则该要素将放在父视图模型中。fetchPostsviewDidLoadfetchPostsdeletePost
1赞 Daniel T. 8/8/2023
如果删除是一项功能,则 tableView 受到用户操作的影响,选项 (2) 是更好的选择。