首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >当receiveCompletion出现错误时取消订阅

当receiveCompletion出现错误时取消订阅
EN

Stack Overflow用户
提问于 2021-03-18 19:06:10
回答 3查看 87关注 0票数 2

在我用MVVM-pattern设计的应用程序中,我有一个loginView。如果存在网络或身份验证问题,登录可能会失败。我的目标是捕获错误并显示相应的警报。我定义了警报的枚举,如下所示:

代码语言:javascript
运行
复制
enum Alerts: Identifiable {
    var id: Int {
        return self.hashValue
    }

    case networkError
    case authenticationError
}

视图的实现是:

代码语言:javascript
运行
复制
struct LoginView: View {

    @ObservedObject var viewModel = LoginViewModel()

    var body: some View {
    
        VStack {
            TextField("Enter e-mail address", text: $viewModel.email)
            SecureField("Enter password", text: $viewModel.password)
        
            Button("Log In") {
                viewModel.login()
            }
        }.alert(item: $viewModel.errorAlert, content: { alert in
            switch alert {
            case .networkError:
                return Alert(title: Text("Error"), message: Text("Check internet Connection"), dismissButton: .default(Text("Ok")))
            case .authenticationError:
                return Alert(title: Text("Error"), message: Text("Some error occured, please try again"), dismissButton: .default(Text("Ok")))
            }
        })
     }
 }

viewModel是:

代码语言:javascript
运行
复制
class LoginViewModel: ObservableObject {

    @Published var email: String = ""
    @Published var password: String = ""

    @Published var errorAlert: Alerts? = nil
    @Published var token: Token? = nil

    var authentication = PassthroughSubject<User, WebserviceError>()

    var cancellables = Set<AnyCancellable>()

    init() {
    
        authentication.map { Webservice().authenticate($0) }.switchToLatest().print().sink { error in
            self.errorAlert = Alerts.networkError
        } receiveValue: { token in
            self.token = token
        }.store(in: &cancellables)

        token.map { KeychainWrapper.save(token: $0)}?.sink(receiveCompletion: { error in
            self.errorAlert = Alerts.authenticationError
        }, receiveValue: { _ in
            //
        }).store(in: &cancellables)
    }

    func login() {
        authentication.send(User(username: email, password: password))
    }
}

Webservice的实现

代码语言:javascript
运行
复制
class Webservice {
    func authenticate(_ user: User) -> AnyPublisher<Token, WebserviceError> {
        return Future<Token, WebserviceError> { promis in
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                if user.username.lowercased() == "root" && user.password == "1234" {
                    promis(.success(Token(access: "asdasda", refresh: "sdfsdfsdf", exprationDate: Date().addingTimeInterval(120))))
                } else {
                    promis(.failure(.unknown))
                }
            }
        }.eraseToAnyPublisher()
    }
}

考虑以下场景:用户第一次运行应用程序。在第一次尝试中,她/他输入了错误的用户名/密码。

发生的情况是,用户将看到等同的警告,并可以单击ok将其关闭。问题是,从第二次开始,点击登录按钮后,什么也没有发生。看起来像authentication.map { ....Viewmodel中的}将被永久取消。为什么会这样呢?

EN

回答 3

Stack Overflow用户

发布于 2021-03-18 20:34:24

根据documentation的说法

发布者继续发出元素,直到它正常完成或失败。

这意味着如果您的发布者(authentication: PassthroughSubject)失败,它将无法再向其订阅者发送值。

解决此问题的一种方法可能是手动调用Webservice().authenticate($0),并直接使用它返回的发布者(并完全删除身份验证主题)。

注:我不是合并专家,我不确定这是否是管理发布者的正确方式(如果经常存储可取消的内容存在某种性能问题),您可能需要深入研究一下。

代码语言:javascript
运行
复制
class LoginViewModel: ObservableObject {

    // [...]

    func login() {
        Webservice()
            .authenticate(User(username: email, password: password))
            .print()
            .sink { error in
                self.errorAlert = Alerts.networkError
            } receiveValue: { token in
                self.token = token
            }
            .store(in: &cancellables)
    }
}
票数 0
EN

Stack Overflow用户

发布于 2021-03-18 21:33:32

Combine publishes会发布值,直到完成或出错--这是设计好的。

在您的用例中,您似乎希望“处理”sink订阅中的错误。换句话说,你的管道永远不会出错。要实现这一点,您可以将值或错误“打包”到一个Result中,并将其作为一个值发出,而就组合管道而言,它具有一个Never故障类型。

为方便起见,让我们创建一个操作符asResult()

代码语言:javascript
运行
复制
extension Publisher {
    func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
        self
            .map { .success($0) }
            .catch { err in Result.Publisher(.failure(err))}
            .eraseToAnyPublisher()
    }
}

您可以将其应用于authenticate

代码语言:javascript
运行
复制
authentication
   .map { 
      Webservice().authenticate($0).asResult()
   }
   .switchToLatest()
   .sink { result in
      // handle the result here
      switch result {
      case .success(let token):
         self.token = token
      case .failure(let error):
         print(error)
         self.errorAlert = Alerts.networkError
      }
   }
   .store(in: &cancellables)

正如你所看到的,上面的管道从来没有失败过--也就是有Failure == Never

票数 0
EN

Stack Overflow用户

发布于 2021-03-18 22:02:51

发布者一旦失败就会停止发送值,这是预期的行为。

您可以通过从登录函数发送身份验证请求来实现此目的。如果发布者失败,您将向用户显示一个错误,他们将能够更改其输入并再次点击登录。这将导致产生新的请求:

代码语言:javascript
运行
复制
class LoginViewModel: ObservableObject {

    @Published var email: String = ""
    @Published var password: String = ""

    @Published private (set) var errorAlert: Alerts? = nil
    @Published private (set) var token: Token? = nil

    private let webservice: Webservice
    private var cancellables = Set<AnyCancellable>()

    init(webservice: Webservice) {
        self.webservice = webservice
        
        $token
            .sink { token in
                KeychainWrapper.save(token: token)
            }
            .store(in: &cancellables)
    }

    func login() {
        let user = User(username: email, password: password)

        webservice
            .authenticate(user)
            .sink(receiveCompletion: { [weak self] completion in
                if completion == .failure(_) {
                    self?.errorAlert = .authenticationError
                }
            }, receiveValue: { [weak self] token in
                self?.token = token
            })
            .store(in: &cancellables)
    }
}
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/66689833

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档