c++ combine使用
Modern app development with SwiftUI and Combine eliminates a lot of boilerplate code. Tools like Playgrounds extend this further to allow quick prototyping. One common issue is to load arbitrary content (JSON or binary) from network and display it in a SwiftUI view. The other day I was looking for a simple yet elegant way to use in quick prototypes. I came up with a reusable view that can load arbitrary content. After a couple of iterations I discovered some interesting tricks to share.
使用SwiftUI和Combine进行的现代应用程序开发消除了许多样板代码。 诸如Playgrounds之类的工具进一步扩展了此功能,以实现快速原型制作。 一个常见的问题是从网络加载任意内容(JSON或二进制)并将其显示在SwiftUI视图中。 前几天,我在寻找一种用于快速原型的简单而优雅的方法。 我想出了一个可重用的视图,可以加载任意内容。 经过几次迭代,我发现了一些有趣的窍门。
内容加载设计 (Content Loading Design)
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/4754ace5efb05014eadc0e2f44c9b520.png)
Content loading is at least three stage process:
内容加载至少分为三个阶段:
- Before the loading starts there is the initial moment - after objects were created, and before a trigger to load the content; 在加载开始之前有一个初始时刻-创建对象之后以及加载内容的触发器之前。
- The actual process of loading content. We need to present some progress or activity indication in the UI; 加载内容的实际过程。 我们需要在用户界面中显示一些进度或活动指示;
- And finally the result, success or failure. 最后是结果,成功或失败。
SwiftUI encourages creating small, reusable views, and use composition to create the complete picture. Each stage of the content loading process will require a view. The container view will compose the result.
SwiftUI鼓励创建小的可重复使用的视图,并使用合成来创建完整的图片。 内容加载过程的每个阶段都需要一个视图。 容器视图将构成结果。
The content loading process can be represented using an enum with associated values. Swift provides Result
type, that could be used to represent completed process. However, I find it more convenient to represent the result as separate success
and failure
cases. This is due to added support of switch
statement in function builders (Xcode 12).
可以使用带有关联值的枚举来表示内容加载过程。 Swift提供了Result
类型,可用于表示已完成的过程。 但是,我发现将结果表示为单独的success
和failure
案例更为方便。 这是由于在函数生成器(Xcode 12)中增加了对switch
语句的支持。
It is easy to see what we are aiming for: the container view can switch over the loading state to provide a corresponding view.
很容易看出我们的目标:容器视图可以切换加载状态以提供相应的视图。
enum RemoteContentLoadingState<Value> {
case initial
case inProgress
case success(_ value: Value)
case failure(_ error: Error)
}
在SwiftUI视图中加载内容 (Loading Content in SwiftUI View)
You probably know by now that a view in SwiftUI can not load content by itself. This is because SwiftUI views are value types. Content loading requires back and forth communication possible only with reference types.
您现在可能知道,SwiftUI中的视图本身无法加载内容。 这是因为SwiftUI视图是值类型。 内容加载仅需要参考类型才能进行来回通信。
The way to provide remote content to a SwiftUI view is by using ObservedObject
property wrapper and ObservableObject
protocol.
向SwiftUI视图提供远程内容的方法是使用ObservedObject
属性包装器和ObservableObject
协议。
ObservableObject
protocol synthesizes a publisher that emits before the object has changed.
ObservableObject
协议综合了在对象更改之前发出的发布者。
Typically, you would create a class that confirms to ObservableObject
and reference it using ObservedObject
property wrapper from your view.
通常,您将创建一个确认ObservableObject
的类,并从视图中使用ObservedObject
属性包装器对其进行引用。
Because we are building a reusable view it makes sense to inject ObservableObject
. To start, declare RemoteContent
protocol.
因为我们正在构建一个可重用的视图,所以注入ObservableObject
是有意义的。 首先,声明RemoteContent
协议。
protocol RemoteContent : ObservableObject {
associatedtype Value
var loadingState: RemoteContentLoadingState<Value> { get }
func load()
func cancel()
}
Associated type Value
is used with generic RemoteContentLoadingState
type. load
and cancel
methods are self-explanatory.
关联类型Value
与通用RemoteContentLoadingState
类型一起使用。 load
和cancel
方法是不言自明的。
Using associated type creates limitations on how the protocol can be used. To freely declare properties using RemoteContent
type it is necessary to apply type erasure technique.
使用关联类型会限制协议的使用方式。 要使用RemoteContent
类型自由声明属性,必须应用类型擦除技术。
final class AnyRemoteContent<Value> : RemoteContent {
init<R: RemoteContent>(_ remoteContent: R) where R.ObjectWillChangePublisher == ObjectWillChangePublisher,
R.Value == Value {
objectWillChangeClosure = {
remoteContent.objectWillChange
}
loadingStateClosure = {
remoteContent.loadingState
}
loadClosure = {
remoteContent.load()
}
cancelClosure = {
remoteContent.cancel()
}
}
private let objectWillChangeClosure: () -> ObjectWillChangePublisher
var objectWillChange: ObservableObjectPublisher {
objectWillChangeClosure()
}
private let loadingStateClosure: () -> RemoteContentLoadingState<Value>
var loadingState: RemoteContentLoadingState<Value> {
loadingStateClosure()
}
private let loadClosure: () -> Void
func load() {
loadClosure()
}
private let cancelClosure: () -> Void
func cancel() {
cancelClosure()
}
}
Note, that AnyRemoteContent
must also delegate objectWillChange
publisher to the RemoteContent
object.
请注意, AnyRemoteContent
还必须将objectWillChange
发布者委托给RemoteContent
对象。
远程内容查看 (Remote Content View)
With this objects in place we can build the container view.
有了这个对象,我们可以构建容器视图。
struct RemoteContentView<Value, Empty, Progress, Failure, Content> : View where Empty : View,
Progress : View,
Failure : View,
Content : View
{
let empty: () -> Empty
let progress: () -> Progress
let failure: (_ error: Error, _ retry: @escaping () -> Void) -> Failure
let content: (_ value: Value) -> Content
init<R: RemoteContent>(remoteContent: R,
empty: @escaping () -> Empty,
progress: @escaping () -> Progress,
failure: @escaping (_ error: Error, _ retry: @escaping () -> Void) -> Failure,
content: @escaping (_ value: Value) -> Content) where R.ObjectWillChangePublisher == ObservableObjectPublisher,
R.Value == Value
{
self.remoteContent = AnyRemoteContent(remoteContent)
self.empty = empty
self.progress = progress
self.failure = failure
self.content = content
}
var body: some View {
ZStack {
switch remoteContent.loadingState {
case .initial:
empty()
case .inProgress:
progress()
case .success(let value):
content(value)
case .failure(let error):
failure(error) {
remoteContent.load()
}
}
}
.onAppear {
remoteContent.load()
}
.onDisappear {
remoteContent.cancel()
}
}
@ObservedObject private var remoteContent: AnyRemoteContent<Value>
}
RemoteContentView
uses closures to provide a view for each of the loading states. Associated values of RemoteContentLoadingState
passed as arguments for the respective closures.
RemoteContentView
使用闭包为每个加载状态提供视图。 作为相应闭包的参数传递的RemoteContentLoadingState
关联值。
Init method takes RemoteContent
as the first argument and requires it associated types to match.
Init方法将RemoteContent
作为第一个参数,并要求其关联的类型匹配。
The remoteContent
property of AnyRemoteContent
type uses ObservedObject
property wrapper. This invalidates the view whenever the remoteContent
changes.
AnyRemoteContent
类型的remoteContent
属性使用ObservedObject
属性包装器。 每当remoteContent
更改时,这会使视图无效。
Also, failure
state allows to reload content by delegating a closure to it’s respective view.
同样, failure
状态允许通过将闭包委托给其相应的视图来重新加载内容。
具体的远程内容 (Concrete Remote Content)
We can’t use it just yet. The last piece is to implement RemoteContent
.
我们暂时无法使用。 最后一步是实现RemoteContent
。
Swift provides Decodable
protocol for JSON and Plist deserializable objects. Let’s implement RemoteContent
object that can utilize it.
Swift为JSON和Plist可反序列化的对象提供了Decodable
协议。 让我们实现可以利用它的RemoteContent
对象。
final class DecodableRemoteContent<Value, Decoder> : RemoteContent where Value : Decodable,
Decoder : TopLevelDecoder,
Decoder.Input == Data
{
unowned let urlSession: URLSession
let url: URL
let type: Value.Type
let decoder: Decoder
init(urlSession: URLSession = .shared, url: URL, type: Value.Type, decoder: Decoder) {
self.urlSession = urlSession
self.url = url
self.type = type
self.decoder = decoder
}
@Published private(set) var loadingState: RemoteContentLoadingState<Value> = .initial
func load() {
// Set state to in progress
loadingState = .inProgress
// Start loading
cancellable = urlSession
.dataTaskPublisher(for: url)
.map {
$0.data
}
.decode(type: type, decoder: decoder)
.map {
.success($0)
}
.catch {
Just(.failure($0))
}
.receive(on: RunLoop.main)
.assign(to: \.loadingState, on: self)
}
func cancel() {
// Reset loading state
loadingState = .initial
// Stop loading
cancellable?.cancel()
cancellable = nil
}
private var cancellable: AnyCancellable?
}
DecodableRemoteContent
uses Combine to load and decode remote content. Combine provides decode<Item, Coder>
function and this is basically why init takes two generic arguments type and decoder.
DecodableRemoteContent
使用Combine来加载和解码远程内容。 Combine提供了decode<Item, Coder>
函数,这基本上就是为什么init带有两个通用参数类型和解码器的原因。
The current loading stage is stored in loadingState
property. Published
property wrapper used to automatically emit when the property changes.
当前加载阶段存储在loadingState
属性中。 Published
属性包装器,用于在属性更改时自动发出。
Actual content loading is a chain of Combine publishers and operators. URLSession
exposes a publisher for a data task. The result is decoded and mapped to RemoteContentLoadingState
type.
实际内容加载是由合并发布者和运营商组成的链。 URLSession
为数据任务公开发布者。 结果被解码并映射到RemoteContentLoadingState
类型。
Lastly, receiving and assigning the result value to the property must be on the main thread.
最后,接收结果并将结果值分配给属性必须在主线程上。
We can also provide JSONDecoder
by default using extension.
我们还可以默认使用扩展名提供JSONDecoder
。
extension DecodableRemoteContent where Decoder == JSONDecoder {
convenience init(urlSession: URLSession = .shared, url: URL, type: Value.Type) {
self.init(urlSession: urlSession, url: url, type: type, decoder: JSONDecoder())
}
}
扩展远程内容视图 (Extending Remote Content View)
Almost there. Remember, SwiftUI encourages creating small, reusable views. You probably have a number of auxilary views, like spinners to indicate activity, or messages to show errors. RemoteContentView
can be extended to provide this views as default arguments.
差不多了。 请记住,SwiftUI鼓励创建小的可重用视图。 您可能有许多辅助视图,例如用于指示活动的微调框或用于显示错误的消息。 可以扩展RemoteContentView
以提供此视图作为默认参数。
extension RemoteContentView where Empty == EmptyView, Progress == ActivityIndicator, Failure == Text {
init<R: RemoteContent>(remoteContent: R,
content: @escaping (_ value: Value) -> Content) where R.ObjectWillChangePublisher == ObservableObjectPublisher,
R.Value == Value
{
self.init(remoteContent: remoteContent,
empty: { EmptyView() },
progress: { ActivityIndicator() },
failure: { error, _ in Text(error.localizedDescription) },
content: content)
}
}
One downside is that this approach requires providing all combinations of generic views to customize individual views. However, this is fairly easy to do, and can be done adhoc.
缺点是这种方法要求提供通用视图的所有组合以自定义单个视图。 但是,这很容易做到,并且可以临时完成。
ActivityIndicator
is a custom wrapper for UIActivityIndicator
that you can find in the package repository.
ActivityIndicator
是 UIActivityIndicator
的自定义包装 ,您可以在软件包存储库中找到它。
使用远程内容视图 (Using Remote Content View)
Finally, we got to the point where we can see how to use RemoteContentView
, in the end this is what matters.
最终,我们可以看到如何使用RemoteContentView
,最终这才是最重要的。
import SwiftUI
import RemoteContentView
struct Post : Codable {
var id: Int
// ...
}
struct PostView : View {
// ...
}
struct PostsView : View {
var body: some View {
let content = DecodableRemoteContent(url: URL(string: "https://jsonplaceholder.typicode.com/posts")!,
type: [Post].self)
return RemoteContentView(remoteContent: content) {
List($0, id: \Post.id) {
PostView(post: $0)
}
}
}
}
Voilà, the list of posts displayed in the List
view. And also activity indication and error handling. Now you can concentrate on implementing the UI for your content. Your reusable content loading view will take care of everything else.
Voilà, List
视图中显示的帖子List
。 以及活动指示和错误处理。 现在,您可以集中精力为您的内容实现UI。 您可重用的内容加载视图将处理其他所有内容。
![Image for post](https://miro.medium.com/max/9999/1*_Bq2pSizuEdWo91xnXyhSQ.png)
As a summary I would like to highlight how using Swift generics allows creating reusable and customizable views; improvements in function builders allow writing straight forward view builder code; and Combine makes communication between objects simple.
作为总结,我想强调一下使用Swift泛型如何允许创建可重用和可定制的视图。 函数构建器的改进允许编写直接视图构建器代码; and Combine使对象之间的通信变得简单。
The RemoteContentView
is a Swift package available on GitHub: https://github.com/dmytro-anokhin/remote-content-view. It includes RemoteImage
to load images and new for iOS 14 redacted placeholder. You can find more examples in provided playgrounds.
RemoteContentView
是可在GitHub上使用的Swift软件包: https : //github.com/dmytro-anokhin/remote-content-view 。 它包括用于加载图像的RemoteImage
和iOS 14修订版占位符的新增功能。 您可以在提供的游乐场中找到更多示例。
Hope you find the article and the package useful. Maybe take a bits of it to improve your SwiftUI views.
希望您发现本文和软件包有用。 也许要花点时间来改善您的SwiftUI视图。
Till next time, bye!
下次再见!
c++ combine使用