【问题标题】:subscription cancels when receiveCompletion with error当 receiveCompletion 出错时订阅取消
【发布时间】:2021-03-18 11:06:09
【问题描述】:

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

enum Alerts: Identifiable {
    var id: Int {
        return self.hashValue
    }

    case networkError
    case authenticationError
}

视图的实现是:

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 是:

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))
    }
}

网络服务的实现

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()
    }
}

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

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

【问题讨论】:

    标签: ios swift swiftui combine


    【解决方案1】:

    根据documentation

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

    这意味着如果您的发布者 (authentication: PassthroughSubject) 失败,它就不能再向其订阅者发出值。

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

    注意:我不是 Combine 专家,我不确定这是否是管理发布者的正确方法(如果频繁存储可取消元素存在某种性能问题),您可能想要深入研究。

    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)
        }
    }
    

    【讨论】:

      【解决方案2】:

      Combine 发布的值会在完成或错误之前发布 - 这是设计使然。

      在您的用例中,您似乎想要“处理”sink 订阅中的错误。换句话说,您的管道永远不应该出错。为此,您可以将值或错误“打包”到 Result 中并将其作为值发出,同时就组合管道而言具有 Never 失败类型。

      为了方便,我们创建一个运算符asResult()

      extension Publisher {
          func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
              self
                  .map { .success($0) }
                  .catch { err in Result.Publisher(.failure(err))}
                  .eraseToAnyPublisher()
          }
      }
      

      你可以将它应用到authenticate:

      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

      【讨论】:

        【解决方案3】:

        发布者在失败后停止发出值是预期的行为。

        您可以通过从登录功能发送验证请求来解决此问题。如果发布者失败,您将向用户显示错误,他们将能够更改输入并再次点击登录。这将导致发出新的请求:

        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)
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2020-08-29
          • 1970-01-01
          • 2021-02-25
          • 1970-01-01
          • 1970-01-01
          • 2016-06-15
          • 1970-01-01
          • 1970-01-01
          • 2016-12-25
          相关资源
          最近更新 更多