美文网首页从0到1学习SwiftUI收藏
一些 Combine 的实际场景

一些 Combine 的实际场景

作者: 一粒咸瓜子 | 来源:发表于2022-01-12 20:14 被阅读0次

摘自《SwiftUI和Combine编程》---《SwiftUI中的Combine》

对于通过 Action 改变的状态,如果我们想要执行网络请求这样的副作用,可以通过同时返回合适的 AppCommand 完成。但是对于通过绑定来更新的状态,由于不会经过 Store 的 reduce 方法来返回 Command,需要我们自驱动。

场景

检测邮箱有效性

用户输入是否有效
是否已被注册(注册时需校验)

思路:

  • 1> 在需要观察的属性前面加上 @Published:
    accountBehavior email
    • @Published 需要在内部生成并持有存储,因此只能针对定义在 class 里的变量添加 @Published
    • 如果属性是在 struct 中,就要将其提取到 class
struct Settings {
    //...
    class AccountChecker {
        @Published var accountBehavior = AccountBehavior.login
        @published var email = ""
    }
    // 声明变量持有这个 class
    var checker = AccountChecker()
    // ...
}
  • 2> 将 accountBehavior 和 email 两个状态合并
var emailCheckPublisher: AnyPublisher<Bool, Never> {
    // 每当email有变化
    let emailPublisher: AnyPublisher<String, Never> = $email
        .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
        .removeDuplicates().eraseToAnyPublisher()
    // 每当behavior有变化
    let behaviorPublisher: AnyPublisher<Bool, Never> = $accountBehavior.map { $0 == .login }
        .eraseToAnyPublisher()
    // 信号合并
    let remoteVerify: AnyPublisher<Bool, Never> = Publishers.CombineLatest(emailPublisher, behaviorPublisher)
        .flatMap { email, isLogin -> AnyPublisher<Bool, Never> in
            let isRegular = email.isValidEmailAddress
            let isLogin = isLogin
            
            switch (isRegular, isLogin) {
            case (false, _): // 输入不合规直接返回false
                return Just(false).eraseToAnyPublisher()
            case (true, false): // 注册行为,输入合规,api判断账号是否已存在
                return EmailCheckRequest(email: email)
                    .publisher.eraseToAnyPublisher()
            case (true, true): // 登录行为,输入合规返回true
                return Just(true).eraseToAnyPublisher()
            }
        }.eraseToAnyPublisher()
    // 短路作用
    let localVaild = $email.map { $0.isValidEmailAddress }
    let behaviorValid = $accountBehavior.map { $0 == .login }
    
    return Publishers.CombineLatest3(localVaild, behaviorValid, remoteVerify)
        .map { $0 && ($1 || $2) }
        .eraseToAnyPublisher()
}

  • 3> 非Action触发,由 Store 自身监听,驱动相关 UI
class Store: ObservableObject {
    init() {
        setupObserver()
    }
    
    func setupObserver() {
        appState.settings.checker.emailCheckPublisher.sink {
            isValid in
            self.dispatch(.emailCheck(valid: isValid))
        }.store(in: &disposeBag)
    }
}


TextField("电子邮箱", text: settingsBinding.checker.email)
    .foregroundColor(settings.isEmailVaild ? .gray : .red)

本地验证密码

检查 password 和 verifyPassword 不为空,而且两者的值相等。

var passwordVerifyPublisher: AnyPublisher<Bool, Never> {
    let canSkip = $accountBehavior.map { $0 == .login }
    return Publishers.CombineLatest3(canSkip, $password, $verifyPassword)
        .flatMap { canSkip, pwd1, pwd2 -> AnyPublisher<Bool, Never> in
            let isRegular = (pwd1.count > 0) && (pwd1 == pwd2)
            switch(canSkip, isRegular) {
            case (true, _):
                return Just(true).eraseToAnyPublisher()
            case (false, _):
                return Just(isRegular).eraseToAnyPublisher()
            }
        }.eraseToAnyPublisher()
}

多重请求的处理

species 请求依赖于 pokemon 请求,请求1~30号数据

struct LoadPokemonRequest {
    let id: Int
    private var base = "https://pokeapi.co/api/v2/pokemon/"
    
    private func pokemonPublisher(_ id: Int) -> AnyPublisher<Pokemon, Error> {
        URLSession.shared
            .dataTaskPublisher(for: URL(string: "\(base)\(id)")!)
            .map { $0.data }
            .decode(type: Pokemon.self, decoder: appDecoder)
            .eraseToAnyPublisher()
    }
    
    private func speciesPublisher(_ pokemon: Pokemon) -> AnyPublisher<(Pokemon, PokemonSpecies), Error> {
        URLSession.shared
            .dataTaskPublisher(for: pokemon.species.url)
            .map { $0.data }
            .decode(type: PokemonSpecies.self, decoder: appDecoder)
            .map { (pokemon, $0) }
            .eraseToAnyPublisher()
    }
    
    // 向外仅提供 publisher,内部将多重请求处理好
    var publisher: AnyPublisher<PokemonViewModel, AppError> {
        pokemonPublisher(id)
            .flatMap { speciesPublisher($0) }
            .map { PokemonViewModel(pokemon: $0, species: $1) }
            .mapError { AppError.networkError($0) }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    // 将 publisher 打包成一个,所有信号都发布后才会收到结果
    static var all: AnyPublisher<[PokemonViewModel], AppError> {
        (1...30).map { LoadPokemonRequest(id: $0).publisher }.zipAll
    }
    
}

extension Array where Element: Publisher {
    var zipAll: AnyPublisher<[Element.Output], Element.Failure> {
        let initial = Just([Element.Output]())
            .setFailureType(to: Element.Failure.self)
            .eraseToAnyPublisher()
        return reduce(initial) { result, publisher in
            result .zip(publisher) { $0 + [$1] }.eraseToAnyPublisher()
        }
    }
}

数据来源不同

abilities 可能一部分已请求过存于本地,一部分需要发起请求
需要在 command 中处理数据源

struct LoadAbilityCommand: AppCommand {
    let pokemon: Pokemon
    
    // 处理 execute 要处理的 publisher
    func load(pokemonAbility: Pokemon.AbilityEntry, in store: Store) -> AnyPublisher<AbilityViewModel, AppError> {
        
        let id = pokemonAbility.ability.url.extractedID!
        // 如果本地存在
        if let value = store.appState.pokemonList.abilities?[id] {
            return Just(value)
                .setFailureType(to: AppError.self)
                .eraseToAnyPublisher()
        } else {
            // 本地不存在
            return LoadAbilityRequest(pokemonAbility: pokemonAbility).publisher
        }
    }
    
    func execute(in store: Store) {
        let token = SubscriptionToken()
        pokemon.abilities
            .map { load(pokemonAbility: $0, in: store) }
            .zipAll
            .sink(receiveCompletion: {
                if case .failure(let error) = $0 {
                    store.dispatch(.loadAbilitiesDone(result: .failure(error)))
                }
                token.unseal()
            }, receiveValue: {
                store.dispatch(.loadAbilitiesDone(result: .success($0)))
            }).seal(in: token)
    }
}

相关文章

网友评论

    本文标题:一些 Combine 的实际场景

    本文链接:https://www.haomeiwen.com/subject/hgfgcrtx.html