c++ combine使用_使用swiftui和Combine构建可重用的内容加载视图

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
Content Loading stages: initial, in progress, success, and failure
内容加载阶段:初始,进行中,成功和失败

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类型,可用于表示已完成的过程。 但是,我发现将结果表示为单独的successfailure案例更为方便。 这是由于在函数生成器(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类型一起使用。 loadcancel方法是不言自明的。

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
The list of posts loaded in the RemoteContentView
在RemoteContentView中加载的帖子列表

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!

下次再见!

翻译自: https://medium.com/flawless-app-stories/building-reusable-content-loading-view-with-swiftui-and-combine-f4886fe77e2b

c++ combine使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值