协议里面可以定义属性吗
This article is originally published at https://swiftsenpai.com on May 17, 2020.
本文最初于 2020年5月17日 发布在 https://swiftsenpai.com 上。
The Combine framework was introduced in WWDC 2019 and it is mainly used alongside SwiftUI. However, this does not limit us to use the Combine framework on our UIKit apps.
Combine框架是在WWDC 2019中引入的,主要与SwiftUI一起使用。 但是,这并不限制我们在UIKit应用程序上使用Combine框架。
In fact, the @Published
property wrapper introduced in Combine is the perfect fit for any UIKit apps with MVVM architecture. We can use @Published
to elegantly link up the view controller with its view model so that the view controller can be notified automatically by any changes made on the view model.
实际上,在Combine中引入的@Published
属性包装器非常适合任何具有MVVM体系结构的UIKit应用程序。 我们可以使用@Published
优雅地将视图控制器与其视图模型链接起来,以便可以通过对视图模型进行的任何更改自动通知视图控制器。
Everything works nicely until the day where I wanted to define a protocol for my view model in order to achieve polymorphism using protocol-oriented programming. The problem arises because the current Swift version (5.2) does not support property wrapper definition in a protocol.
一切顺利,直到我想为我的视图模型定义协议以使用面向协议的编程实现多态的那天。 出现问题是因为当前的Swift版本(5.2)不支持协议中的属性包装器定义。
How should we go about defining @Published
property wrapper type in a protocol? Read on to find out more.
我们应该如何在协议中定义@Published
属性包装器类型? 请继续阅读以了解更多信息。
问题 (The Problem)
Let’s say we have a view controller and a view model with a @Published
variable called name
. Whenever name
is set or updated, the name
publisher will notify MyViewController
to print out a hello message.
比方说,我们有一个视图控制器和一个视图模型@Published
变量叫name
。 只要name
被设置或更新,该name
出版商将通知MyViewController
打印出hello消息。
class MyViewModel {
@Published var name: String
init(name: String) {
self.name = name
}
}
class MyViewController: UIViewController {
var viewModel: MyViewModel!
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
// Subscribe to the view model's name publisher
// and get notify when name is set or updated
viewModel.$name
.receive(on: RunLoop.main)
.sink { (name) in
// Print hello message when name is set or updated
print("Hello \(name)")
}.store(in: &cancellables)
}
}
To see the above code in action, let’s try to run it using Xcode Playground.
要查看上面的代码,请尝试使用Xcode Playground运行它。
let viewModel = MyViewModel(name: "Swift Senpai 1")
let viewController = MyViewController()
viewController.viewModel = viewModel
PlaygroundPage.current.liveView = viewController
viewModel.name = "Swift Senpai 2"
viewModel.name = "Swift Senpai 3"
// Output:
// Hello Swift Senpai 1
// Hello Swift Senpai 2
// Hello Swift Senpai 3
As you can see from the output, the name
publisher is working according to our expectation.
从输出中可以看到, name
发布者正在按照我们的期望工作。
Now, let’s try to improve the reusability of MyViewController
by applying polymorphism on MyViewController
's view model type.
现在,让我们尝试通过对MyViewController
的视图模型类型应用多态来提高MyViewController
的可重用性。
What we can do here is to create a polymorphic interface using protocol so that MyViewController
can accept any view models that conform to this protocol.
我们在这里可以做的是使用协议创建一个多态接口,以便MyViewController
可以接受任何符合该协议的视图模型。
// Define a polymorphic interface
protocol ViewModelProtocol {
@Published var name: String { get }
}
// Conform to ViewModelProtocol
class MyViewModel: ViewModelProtocol {
@Published var name: String
init(name: String) {
self.name = name
}
}
class MyViewController: UIViewController {
// Change viewModel type to ViewModelProtocol
var viewModel: ViewModelProtocol!
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$name
.receive(on: RunLoop.main)
.sink { (name) in
print("Hello \(name)")
}.store(in: &cancellables)
}
}
Unfortunately, when the code above is executed, the compiler will start complaining about “Property ‘name’ declared inside a protocol cannot have a wrapper”.
不幸的是,执行上述代码时,编译器将开始抱怨“ 协议内声明的属性'name'不能具有包装器 ”。
![Compiler error in Xcode when define protocol with property wrapper](https://i-blog.csdnimg.cn/blog_migrate/a67274d35cf2cd0f8cf6c8a3f16fbe93.png)
This is because the current version of Swift (5.2) does not support property wrapper definition in a protocol. 🙁
这是因为当前版本的Swift(5.2)不支持协议中的属性包装器定义。 🙁
解决方法 (The Workaround)
In order to work around this limitation, let’s take a step back and understand how we are using the @Published
name
property wrapper in the view controller.
为了解决此限制,让我们退后一步,了解我们如何在视图控制器中使用@Published
name
属性包装器。
// Subscribe to the view model's name publisher
// and get notify when name is set or updated
viewModel.$name
.receive(on: RunLoop.main)
.sink { (name) in
print("Hello \(name)")
}.store(in: &cancellables)
As seen in the code snippet above, we are assessing the name
publisher via the projected value of @Published
name
property wrapper. In order word, what we actually need in the view controller is the name
publisher but not the @Published
name
property wrapper.
如上面的代码片段所示,我们正在通过@Published
name
属性包装器的预计值评估name
发布者。 换句话说,视图控制器中实际上需要的是name
发布者,而不是@Published
name
属性包装器。
With that in mind, we are now able to work around the limitation by defining a protocol with name
publisher and manually expose the name
publisher in the view model implementation.
考虑到这一点,我们现在可以通过使用name
发布者定义协议来解决限制,并在视图模型实现中手动公开name
发布者。
protocol ViewModelProtocol {
// Define name publisher
var namePublisher: Published<String>.Publisher { get }
}
class MyViewModel: ViewModelProtocol {
@Published var name: String
// Manually expose name publisher in view model implementation
var namePublisher: Published<String>.Publisher { $name }
init(name: String) {
self.name = name
}
}
class MyViewController: UIViewController {
var viewModel: ViewModelProtocol!
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
// Subscribe to the view model's name publisher
// and get notify when name is set or updated
viewModel.namePublisher
.receive(on: RunLoop.main)
.sink { (name) in
print("Hello \(name)")
}.store(in: &cancellables)
}
}
// Execute code
let viewModel = MyViewModel(name: "Swift Senpai 1")
let viewController = MyViewController()
viewController.viewModel = viewModel
PlaygroundPage.current.liveView = viewController
viewModel.name = "Swift Senpai 2"
viewModel.name = "Swift Senpai 3"
// Output:
// Hello Swift Senpai 1
// Hello Swift Senpai 2
// Hello Swift Senpai 3
With that, we have successfully created a polymorphic interface while retaining the behavior of the view model and view controller. 🥳
这样,我们就成功创建了一个多态接口,同时保留了视图模型和视图控制器的行为。 🥳
更进一步 (Taking One Step Further)
The suggested workaround is not only limited to exposing the publisher, we can also use the same concept to expose the @Published
property wrapper itself as well as the wrapped value.
建议的解决方法不仅限于公开发布者,我们还可以使用相同的概念公开@Published
属性包装器本身以及包装的值。
protocol ViewModelProtocol {
// Define name (wrapped value)
var name: String { get }
// Define name Published property wrapper
var namePublished: Published<String> { get }
// Define name publisher
var namePublisher: Published<String>.Publisher { get }
}
class MyViewModel: ViewModelProtocol {
@Published var name: String
var namePublished: Published<String> { _name }
var namePublisher: Published<String>.Publisher { $name }
// ... ...
// ... ...
// ... ...
}
结语 (Wrapping Up)
As you can see from the above example, the limitation that we are facing is more of a property wrapper limitation rather than the @Published
type limitation.
从上面的示例中可以看到,我们面临的限制更多是属性包装器限制,而不是@Published
类型限制。
Therefore, with the same idea, you can definitely apply the workaround to any kind of property wrapper. I’ll leave that as an exercise for you!
因此,以相同的想法,您绝对可以将变通方法应用于任何类型的属性包装器。 我将其留给您练习!
If you have any questions, feel free to leave it in the comment section below or you can reach out to me on Twitter.
如有任何疑问,请随时在下面的评论部分中保留,或者可以在Twitter上与我联系。
Thanks for reading. 🧑🏻💻
谢谢阅读。 💻
进一步阅读 (Further Readings)
翻译自: https://medium.com/swlh/how-to-define-a-protocol-with-published-property-wrapper-type-1b6349097064
协议里面可以定义属性吗