这个委托模式在SwiftUI中仍然有效吗?

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

Is this delegate pattern still valid in SwiftUI?

问题

我已经用Swift开发应用程序几年了。我一直在使用Cocoa,但最近决定将它们迁移到SwiftUI。我很快就爱上了SwiftUI,但每次我使用SwiftUI开发时,我都会问自己同样的问题 - 在使用SwiftUI时,这些旧代码是否仍然有效,或者在使用MVVM构建的应用程序中使用它时,我的委托模式是否不再是最佳实践?

我现在正在将使用控制器托管计时器的应用程序迁移到SwiftUI。计时器将每秒触发一次并更新其模型。

我写了一个小例子,所以让我们考虑一下这段代码:

// 托管计时器的控制器。
protocol ControllerDelegate : AnyObject {
func controller(didRefresh value: Double)
}

class Controller {
private var timer: Timer?

weak var delegate: ControllerDelegate?

@Published var value: Double = 0.0

init() {
    self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.refresh), userInfo: nil, repeats: true)
}

@objc private func refresh() {
    // 做一些事情。
    self.delegate?.controller(didRefresh: 10.0) // 将刚刚完成的任何操作的结果传递给ViewModel。这里的值仅为示例,可以是任何内容。
}

}

我有一个主视图模型,可能看起来像这样:

// 视图模型
class ViewModel : ObservableObject {
private var controller = Controller()

@Published var value: Double = 0.0

init() {
    self.controller.delegate = self
}

}

extension Controller : ControllerDelegate {
func controller(didRefresh value: Double) {
self.value = value
}
}

视图模型具有一个控制器的实例,并充当委托。它还具有将绑定到视图的Published属性,如下所示:

// 视图
struct ExampleView : View {
@ObservedObject var viewModel: ViewModel

var body: some View {
    Text(self.viewModel.value.description)
}

}

虽然这样做相当不错,但这真的是正确的方法吗?这就是Combine的用途吗,即Combine是否可以替代委托模式?使用Combine时,这段代码会是什么样子?或者Combine是否有其他用途?

问候

英文:

I have been developing apps using Swift for a couple of years now. I always used Cocoa but recently decided to finally port them over to SwiftUI. I fell in love with SwiftUI pretty quickly, but whenever I develop using SwiftUI, I also ask myself the same question - is this old code still valid when using SwiftUI or this my delegate pattern no longer best practice when using it in a MVVM based app?

I am now porting my apps to SwiftUI that use a controller that hosts a timer. The timer will fire once a second and updates its model.

I wrote a small example, so let's consider this code:

// Controller that hosts the timer.
protocol ControllerDelegate : AnyObject {
    func controller(didRefresh value: Double)
}

class Controller {
    private var timer: Timer?

    weak var delegate: ControllerDelegate?

    @Published var value: Double = 0.0

    init() {
        self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.refresh), userInfo: nil, repeats: true)
    }

    @objc private func refresh() {
        // Do some stuff.
        self.delegate?.controller(didRefresh: 10.0) // Pass the result of whatever we just did to the ViewModel. Values are just an example, this could be anything.
    }
}

I have a Main View Model that could look like this:

// View Model
class ViewModel : ObservableObject {
    private var controller = Controller()

    @Published var value: Double = 0.0

    init() {
        self.controller.delegate = self
    }
}

extension Controller : ControllerDelegate {
    func controller(didRefresh value: Double) {
        self.value = value
    }
}

The View Model has an instance of the controller and acts as a delegate. It also has a Published property that will be bound to views, like this:

// View
struct ExampleView : View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Text(self.viewModel.value.description)
    }
}

While this works quite well - is this really the way to go? Is this what Combine is for, i.e. could Combine replace the delegate pattern? What would this code look like when using Combine? Or does Combine serve another purpose?

Regards

答案1

得分: 1

我会将代码部分翻译如下:

// 视图模型
@MainActor class ViewModel : ObservableObject { // <-- 将其设为 MainActor(这是一些注释,与你的问题无直接关联)
    
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // <-- 使用此定时器

    @Published var value: Double = 0.0
    
    func refresh() { // <-- 此函数将在定时器上调用
        // 进行一些操作。
        let value = Double.random(in: 1.0...10.0) // 将刚刚执行的操作结果传递给视图模型。这里的值仅为示例,可以是任何值。
        self.value = value
    }
}

然后是用户界面:

// 视图
struct ExampleView : View {
    
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Text(self.viewModel.value.description)
            .onReceive(viewModel.timer) { _ in // <-- 在定时器上执行
                viewModel.refresh()
            }
    }
}

这就是全部内容,不需要控制器或委托。

英文:

I'd simplify it like this:

// View Model
@MainActor class ViewModel : ObservableObject { // <-- make it a MainActor (this is side note, not directly related to your question)
    
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // <-- use this timer instead

    @Published var value: Double = 0.0
    
    func refresh() { // <-- this function will be called on timer
        // Do some stuff.
        let value = Double.random(in: 1.0...10.0) // Pass the result of whatever we just did to the ViewModel. Values are just an example, this could be anything.
        self.value = value
    }
}

And then UI:

// View
struct ExampleView : View {
    
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Text(self.viewModel.value.description)
            .onReceive(viewModel.timer) { _ in // <-- executed on timer
                viewModel.refresh()
            }
    }
}

That's it. No need for controller or delegate

答案2

得分: 1

这是一个使用Combine的示例:

import UIKit
import SwiftUI
import Combine
import PlaygroundSupport

class TimerModel: ObservableObject {
    let value: AnyPublisher<Double, Never>

    init() {
        let timer = Timer.publish(every: 1.0, on: RunLoop.main, in: .common).autoconnect()
        value = timer.map { _ in
            // 执行一些操作
            return Double.random(in: (1.0...100.0))
        }.eraseToAnyPublisher()
    }
}

// 视图
struct ExampleView: View {
    @StateObject var viewModel: TimerModel = TimerModel()
    @State var somePercentage: Double = 0

    var body: some View {
        HStack {
            Text(String(describing: somePercentage))
            Text("%")
        }.onReceive(viewModel.value) { value in
            debugPrint(value)
            somePercentage = value
        }.frame(width: 300, height: 200)
    }
}

let exampleView = ExampleView()

PlaygroundSupport.PlaygroundPage.current.liveView = UIHostingController(rootView: exampleView)

使用Combine思考这个示例的方式是,你实际上不太关心定时器。你真正关心的是值的流(在这种情况下是Double值的流)。

因此,在这个示例中,模型拥有一个定时器作为模型值的源,但模型实际上暴露了组成模型的值的流。

委托模式没有问题。它仍然是一个完全有效的工具。但你可以以多种不同的方式实现这一点。

英文:

Here's an example of how you might use Combine:

import UIKit
import SwiftUI
import Combine
import PlaygroundSupport

class TimerModel: ObservableObject {
    let value : AnyPublisher&lt;Double, Never&gt;

    init() {
        let timer = Timer.publish(every: 1.0, on: RunLoop.main, in: .common).autoconnect()
        value = timer.map { _ in
            // Do some stuff
            return Double.random(in: (1.0...100.0))
        }.eraseToAnyPublisher()
    }
}

// View
struct ExampleView : View {
    @StateObject var viewModel: TimerModel = TimerModel()
    @State var somePercentage: Double = 0

    var body: some View {
        HStack {
            Text(String(describing: somePercentage))
            Text(&quot;%&quot;)
        }.onReceive(viewModel.value) { value in
            debugPrint(value)
            somePercentage = value
        }.frame(width: 300, height: 200)
    }
}

let exampleView = ExampleView()

PlaygroundSupport.PlaygroundPage.current.liveView = UIHostingController(rootView: exampleView)

A way to think about this with Combine is that you really don't care about the timer. What you really care about is a stream of values (in this case a stream of Doubles).

So in this example, the model owns a timer as the source of a model value, but the model really exposes the stream of values that make up the model.

There's nothing wrong with the delegate pattern. It's still a perfectly valid tool. But you could implement this in a number of different ways.

答案3

得分: 0

在SwiftUI中,没有委托或视图模型对象,View 结构体已经封装了 SwiftUI 用于初始化/反初始化/更新 UIView 对象的视图数据,即实际的 V 层,它会自动为您处理,因此MVVM是不相关的。

对于通过定时器重新计算的 View 结构体,请尝试使用 TimelineView

> TimelineView 一个根据您提供的时间表更新的视图。
>
> 概述 时间轴视图充当一个没有自己外观的容器。相反,它会在预定的时间点重新绘制其包含的内容。

英文:

There are no delegates or view model objects in SwiftUI the View struct already encapsulates view data that SwiftUI uses to init/deinit/update UIView objects, i.e. the actual V layer, automatically for you, so MVVM is irrelevant.

For a View struct that recomputes via a timer try TimelineView.

> TimelineView A view that updates according to a schedule that you
> provide.
>
> Overview A timeline view acts as a container with no appearance of its
> own. Instead, it redraws the content it contains at scheduled points
> in time

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

发表评论

匿名网友

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

确定