提问人:Andy 提问时间:7/27/2023 最后编辑:Andy 更新时间:7/29/2023 访问量:103
如何使用 iOS MVVM 处理子视图中的用户交互
How to handle user interactions in subviews with iOS MVVM
问:
为了实现具有表视图控制器的 MVVM,通常需要为每个单元格使用一个父视图模型和一堆子视图模型。假设每个单元格都有一个“赞”按钮,现在用户点击其中一个“赞”按钮。
在搜索堆栈溢出后,我看到了三种处理流的可能方法:
点击的操作将发送到子视图模型,子视图模型在内部处理类似操作。
点击的操作将发送到子视图模型,子视图模型将意图传递给父视图模型,父视图模型处理类似操作。
点击的操作将发送到表视图控制器(使用单元委托或闭包),然后表视图控制器将点击的操作传递给父视图模型。
就个人而言,我更喜欢第二种和第三种方法。让我感到困惑的是,在第 3 种方法中,子视图模型只负责输出(呈现数据),而不负责输入(处理交互)。相反,父视图模型负责处理子视图的交互。因为通常视图模型会为其视图处理这两个问题。
哪种方法更好?或者有更好的方法来实现同样的目标?
任何建议都会有所帮助。
答:
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
服务器不会将更新的帖子发送回父视图模型。仅当视图首次加载时(在 中调用)才会进行调用。您必须在上面添加代码才能再次调用(也许是拉取刷新?如果要添加要素,则该要素将放在父视图模型中。fetchPosts
viewDidLoad
fetchPosts
deletePost
1赞
Daniel T.
8/8/2023
如果删除是一项功能,则 tableView 会受到用户操作的影响,选项 (2) 是更好的选择。
评论