一些 Combine 的实际场景

摘自《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)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 摘自《SwiftUI和Combine编程》---《SwiftUI架构》 Redux For SwiftUI 架构图...
    一粒咸瓜子阅读 404评论 0 0
  • 这一篇文章给大家介绍:Xcode 11 Beta 5,虽然是beta版本,但是在不久的将来必将来临,例如:Swif...
    Cooci_和谐学习_不急不躁阅读 10,204评论 6 18
  • General New Features Xcode 11 beta supports development w...
    Zszen阅读 3,414评论 0 50
  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,257评论 0 10
  • 数据状态和绑定 上一篇文章没有涉及到如何使用数据让 app 界面真正能被使用。在 SwiftUI 里,用户界面是严...
    微笑_d797阅读 718评论 0 0