联合出版商。在简单登录流程示例中隔离业务逻辑

问题描述 投票:0回答:2

我对Combine还是很陌生,而不是将我的所有任务都运行到viewModel中,而是试图更好地隔离与业务逻辑有关的代码。

让我们以SignIn服务为例。服务接收usernamepassword并返回tokenuserID

[该服务的公开调用是signIn,该调用在内部调用私有函数networkCall。我想实现这两个函数以返回PublishernetworkCall的作用应该是调用API并存储接收到的令牌,而signIn的作用只是返回成功或失败。

这是我的代码,我还在这里突出显示我被卡住的地方。通常,我不知道在哪里使用从API接收到的信息(并存储令牌)的正确位置。目前,我正在.map调用中执行此操作,但这对我来说是错误的。您是否可以分享一些建议以改进此逻辑,尤其是解释哪个是运行业务逻辑的正确位置...我想.map不是正确的位置!并且.sink只会停止链接。

struct SignInResponse:Codable{
    var token:String
    var userID:String
}

class SignInService {

    // Perform the API call
    private func networkCall(with request:SignInRequest)->AnyPublisher<SignInResponse, ServiceError>{
        return URLSession.DataTaskPublisher(request: request, session: .shared)
        .decode(type: SignInResponse.self, decoder: JSONDecoder())
        .mapError{error in return ServiceError.error}
        .eraseToAnyPublisher()
    }

    func signIn(username:String, password:String)->AnyPublisher<Result<String, ServiceError>, Never>{
        let request = SignInRequest(with username:username, password:password)

        return networkCall(with: request)
            .map{ (response) -> Result<String, ServiceError> in

                if response.token != ""{
                    // THIS SOUNDS EXTREMELLY WRONG. I SHOULD NOT USE MAP TO HANDLE THE TOKEN -------
                    self.storage.save(key: "token", value: response.token)
                    return Result.success(response.userID)
                }else{
                    return Result.failure(ServiceError.unknown)
                }
            }
            .replaceError(with: Result.failure(ServiceError.unknown))
            .eraseToAnyPublisher()
    }
    ...... 
}

从模型中,我以这种方式调用SignIn:

func requestsSignIn(){

    if let username = username, let password = password{
        cancellable = service.signIn(username: username, password: password)
            .sink(receiveValue: { (result) in
                switch result{
                case .failure(let error):
                    self.errorMessage = error.localizedDescription
                case .success(let userID):
                    // the sigin succeeded do something here

                }
            })
    }
}
swift combine
2个回答
1
投票

基本上,我同意现有的答案。您在这里的误解似乎是Combine管道的目的所在。这个想法是any一个有用的值—在这里,您的用户ID – or一个错误(如果合适;否则,什么也没有)应该弹出管道的末尾。管道末端的订户准备接收其中任何一个。

因此通常将Result对象传递到管道的末端是没有意义的,必须将其进一步分析为成功或失败值。 Result对象的目的仅仅是允许您传递异步性,即,通过在将来的某个时间将结果交给Result来调用其他人,以免不必使用two]中的一个进行调用。 >值,即使用两个可选参数的实数值或错误。在Combine出版商发布之后,已经发生了异步性,并且您已经收到了这一事实的信号;这就是发布means的内容。您现在唯一需要保留的是信号的任何部分或突变对您都有意义且有用。

这里是一个相当典型的管道,可以完成您想做的事情;我没有像您一样将其分为两部分,但是您当然可以将其分成两个部分:

    URLSession.DataTaskPublisher(request: request, session: .shared)
        .map {$0.data}
        .decode(type: SignInResponse.self, decoder: JSONDecoder())
        .tryMap { response -> String in
            if response.token == "" {
                throw ServiceError.unknown
            }
            return response.userID
        }
        .receive(on:DispatchQueue.main)
        .sink(receiveCompletion: {err in /* do something with error */ },
              receiveValue: {userID in /* do something with userID */})
        .store(in:&storage)

首先,数据任务的结果是一个元组,但是我们需要的只是数据部分,因此我们映射到该部分。然后我们解码。然后,我们检查一个空令牌,如果得到一个,则抛出该令牌。否则,我们向下映射到用户ID,因为这是唯一有用的结果。最后,我们切换到主线程并使用接收器捕获输出,然后将接收器存储在通常的Set<AnyCancellable>中,以便其持续足够长的时间以致发生某些事情。

观察到,如果在我们遇到故障错误的过程中的any

阶段,那个错误立即传播到整个管道末端。如果数据任务失败,它将是URLError。如果解码失败,则与解码器一样,报告该问题将是错误。如果令牌不存在,将出现ServiceError。当然,在此过程中的任何时候,您都可以根据需要捕获和阻止或转换错误,因为它会出现。

作为一种替代设置,signIn返回仅具有输出String和故障类型Service.Error的发布者(Result类型对于发布者而言是多余的)。

然后,对于响应中的错误令牌字符串之类的错误,请使用tryMap而不是map从网络功能转换Result类型,并使其抛出ServiceEror.emptyToken或类似内容。这将导致发布者立即将其发布为失败。


1
投票

作为一种替代设置,signIn返回仅具有输出String和故障类型Service.Error的发布者(Result类型对于发布者而言是多余的)。

© www.soinside.com 2019 - 2024. All rights reserved.