[{"data":1,"prerenderedAt":6829},["ShallowReactive",2],{"navigation_docs":3,"-ios-state-management":173,"-ios-state-management-surround":6824},[4,22,29,36,107,138,145,161],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"博客","i-lucide-pen-line","\u002Fblog","0.blog",[10,12,17],{"title":5,"path":7,"stem":11,"icon":6},"0.blog\u002Findex",{"title":13,"path":14,"stem":15,"icon":16},"创建基于 UIKit 的原生 iOS 项目","\u002Fblog\u002Fcreate-uikit-ios-project","0.blog\u002F01.create-uikit-ios-project","i-lucide-app-window",{"title":18,"path":19,"stem":20,"icon":21},"Lockify：把账号和密码留在自己的手机里","\u002Fblog\u002Flockify","0.blog\u002F02.lockify","i-lucide-lock",{"title":23,"path":24,"stem":25,"children":26,"icon":28},"年度总结","\u002Fannual-review","1.annual-review\u002Findex",[27],{"title":23,"path":24,"stem":25,"icon":28},"i-lucide-git-commit",{"title":30,"path":31,"stem":32,"children":33,"icon":35},"故事","\u002Fstories","2.stories\u002Findex",[34],{"title":30,"path":31,"stem":32,"icon":35},"i-lucide-feather",{"title":37,"icon":38,"path":39,"stem":40,"children":41},"Android","i-simple-icons-android","\u002Fandroid","3.android",[42,44,49],{"title":37,"path":39,"stem":43,"icon":38},"3.android\u002Findex",{"title":45,"path":46,"stem":47,"icon":48},"初探Navigation","\u002Fandroid\u002Fcompose-navigation","3.android\u002F01.compose-navigation","i-lucide-route",{"title":50,"icon":51,"path":52,"stem":53,"children":54},"安卓代码加固","i-lucide-shield","\u002Fandroid\u002Fhidden-hardening","3.android\u002F02.hidden-hardening\u002Findex",[55,58,62,67,72,77,82,87,92,97,102],{"title":56,"path":52,"stem":53,"icon":57},"安卓代码加固：总览","i-lucide-map",{"title":59,"path":60,"stem":61,"icon":21},"安卓代码加固（1）：加密DEX","\u002Fandroid\u002Fhidden-hardening\u002Fencrypt-dex","3.android\u002F02.hidden-hardening\u002F01.encrypt-dex",{"title":63,"path":64,"stem":65,"icon":66},"安卓代码加固（2）：Native解密","\u002Fandroid\u002Fhidden-hardening\u002Fnative-decrypt","3.android\u002F02.hidden-hardening\u002F02.native-decrypt","i-lucide-cpu",{"title":68,"path":69,"stem":70,"icon":71},"安卓代码加固（3）：内存加载","\u002Fandroid\u002Fhidden-hardening\u002Fmemory-load","3.android\u002F02.hidden-hardening\u002F03.memory-load","i-lucide-memory-stick",{"title":73,"path":74,"stem":75,"icon":76},"安卓代码加固（4）：指令分发","\u002Fandroid\u002Fhidden-hardening\u002Fopcode-dispatch","3.android\u002F02.hidden-hardening\u002F04.opcode-dispatch","i-lucide-hash",{"title":78,"path":79,"stem":80,"icon":81},"安卓代码加固（5）：XOR混淆","\u002Fandroid\u002Fhidden-hardening\u002Fxor-obfuscate","3.android\u002F02.hidden-hardening\u002F05.xor-obfuscate","i-lucide-shuffle",{"title":83,"path":84,"stem":85,"icon":86},"安卓代码加固（6）：密钥外置","\u002Fandroid\u002Fhidden-hardening\u002Fkey-external","3.android\u002F02.hidden-hardening\u002F06.key-external","i-lucide-key",{"title":88,"path":89,"stem":90,"icon":91},"安卓代码加固（7）：依赖隔离","\u002Fandroid\u002Fhidden-hardening\u002Fdep-isolate","3.android\u002F02.hidden-hardening\u002F07.dep-isolate","i-lucide-layers",{"title":93,"path":94,"stem":95,"icon":96},"安卓代码加固（8）：构建校验","\u002Fandroid\u002Fhidden-hardening\u002Fbuild-verify","3.android\u002F02.hidden-hardening\u002F08.build-verify","i-lucide-shield-check",{"title":98,"path":99,"stem":100,"icon":101},"安卓代码加固（9）：防录屏","\u002Fandroid\u002Fhidden-hardening\u002Fflag-secure","3.android\u002F02.hidden-hardening\u002F09.flag-secure","i-lucide-eye-off",{"title":103,"path":104,"stem":105,"icon":106},"安卓代码加固（10）：ABI门禁","\u002Fandroid\u002Fhidden-hardening\u002Fabi-gate","3.android\u002F02.hidden-hardening\u002F10.abi-gate","i-lucide-git-compare",{"title":108,"icon":109,"path":110,"stem":111,"children":112},"iOS","i-simple-icons-apple","\u002Fios","4.ios",[113,115,120,125,129,133],{"title":108,"path":110,"stem":114,"icon":109},"4.ios\u002Findex",{"title":116,"path":117,"stem":118,"icon":119},"iOS面试题1","\u002Fios\u002Fios-interview1","4.ios\u002F01.ios-interview1","i-lucide-sparkles",{"title":121,"path":122,"stem":123,"icon":124},"设置启动图","\u002Fios\u002Fset-launch-screen","4.ios\u002F02.set-launch-screen","i-lucide-image",{"title":126,"path":127,"stem":128,"icon":48},"路由管理","\u002Fios\u002Frouting","4.ios\u002F03.routing",{"title":130,"path":131,"stem":132,"icon":91},"状态管理","\u002Fios\u002Fstate-management","4.ios\u002F04.state-management",{"title":134,"path":135,"stem":136,"icon":137},"事件传递","\u002Fios\u002Fevent-delivery","4.ios\u002F05.event-delivery","i-lucide-pointer",{"title":139,"path":140,"stem":141,"children":142,"icon":144},"Flutter","\u002Fflutter","5.flutter\u002Findex",[143],{"title":139,"path":140,"stem":141,"icon":144},"i-simple-icons-flutter",{"title":146,"icon":147,"path":148,"stem":149,"children":150,"page":160},"面试","i-lucide-graduation-cap","\u002Finterview","6.interview",[151,156],{"title":152,"path":153,"stem":154,"icon":155},"Flutter面试题1","\u002Finterview\u002Fflutter-interview1","6.interview\u002F01.flutter-interview1","i-lucide-boxes",{"title":157,"path":158,"stem":159,"icon":66},"Flutter面试题2","\u002Finterview\u002Fflutter-interview2","6.interview\u002F02.flutter-interview2",false,{"title":162,"icon":163,"path":164,"stem":165,"children":166},"算法学习","i-lucide-binary","\u002Falgorithm","7.algorithm",[167,169],{"title":162,"path":164,"stem":168,"icon":163},"7.algorithm\u002Findex",{"title":170,"path":171,"stem":172},"算法基础1：复杂度分析","\u002Falgorithm\u002Ftime-space-complexity","7.algorithm\u002F01.time-space-complexity",{"id":174,"title":130,"body":175,"description":6802,"extension":6803,"links":6804,"meta":6813,"navigation":6819,"path":131,"seo":6820,"stem":132,"__hash__":6823},"docs\u002F4.ios\u002F04.state-management.md",{"type":176,"value":177,"toc":6771},"minimark",[178,182,185,206,211,214,224,227,312,326,330,354,530,534,550,749,752,768,781,1086,1089,1112,1261,1264,1294,1304,1497,1500,1521,1720,1732,1790,1913,1916,1977,1981,1984,1990,2001,2004,2418,2426,2429,2438,2689,2885,2899,2906,2909,2912,3151,3164,3168,3256,3261,3269,3280,3283,3286,3382,3385,3395,3568,3581,3584,3587,3597,3613,3619,3621,3628,3631,3827,3841,3853,3863,3866,3869,3921,3924,3930,3940,3943,3949,4048,4051,4062,4144,4485,4488,4513,4521,4527,4749,4769,4772,4785,4890,4896,5008,5018,5021,5049,5353,5370,5559,5575,5609,5612,5615,5629,5856,5877,5882,5885,5907,6330,6343,6535,6540,6598,6609,6626,6635,6638,6702,6705,6767],[179,180,181],"p",{},"我把状态管理拆成四段记。先问三件事：状态放在哪，谁可以改，改完界面怎么跟上。",[179,183,184],{},"SwiftUI 用属性包装器回答这三问，从近到远是一架梯子。UIKit 没有这架梯子，数据在控制器里，界面要自己写进去。一页里同时堆了请求、校验和跳转，再把状态收成 ViewModel，或者收成「一份值 + 一组动作」。Combine 把变化送出去。RxSwift、ReactorKit、TCA 再规定动作和副作用怎么走。界面最后仍是读这份状态。",[179,186,187,188,192,193,196,197,201,202,205],{},"环境按 ",[189,190,191],"strong",{},"Xcode 16","。SwiftUI 新代码用 ",[189,194,195],{},"iOS 17"," 的 ",[198,199,200],"code",{},"@Observable","。更早的系统用 ",[198,203,204],{},"ObservableObject"," 那一档，文里分开写。",[207,208,210],"h2",{"id":209},"swiftui-阶梯","SwiftUI 阶梯",[179,212,213],{},"梯子排的是所有权。上面一档持有存储，下面一档借用。",[215,216,222],"pre",{"className":217,"code":219,"language":220,"meta":221},[218],"language-text","这一块视图\n  @State                 自己的值\n  @Binding               改上一档的值\n\n这一屏的对象（@Observable）\n  @State                 创建并持有\n  let                    只读，传给子视图\n  @Bindable              子视图要绑定字段\n  @Environment           祖先注入，跨层共用\n\n系统存储\n  @AppStorage            偏好\n  @SceneStorage          这个场景要恢复的界面状态\n","text","",[198,223,219],{"__ignoreMap":221},[179,225,226],{},"iOS 17 之前，对象那几档是另一套名字。",[228,229,230,246],"table",{},[231,232,233],"thead",{},[234,235,236,240,243],"tr",{},[237,238,239],"th",{},"现在",[237,241,242],{},"iOS 17 之前",[237,244,245],{},"谁持有",[247,248,249,268,284,299],"tbody",{},[234,250,251,260,265],{},[252,253,254,257,258],"td",{},[198,255,256],{},"@State"," 拿着 ",[198,259,200],{},[252,261,262],{},[198,263,264],{},"@StateObject",[252,266,267],{},"创建它的视图",[234,269,270,276,281],{},[252,271,272,273],{},"普通参数，或 ",[198,274,275],{},"@Bindable",[252,277,278],{},[198,279,280],{},"@ObservedObject",[252,282,283],{},"外面传进来",[234,285,286,291,296],{},[252,287,288],{},[198,289,290],{},"@Environment",[252,292,293],{},[198,294,295],{},"@EnvironmentObject",[252,297,298],{},"祖先放进环境",[234,300,301,304,309],{},[252,302,303],{},"属性直接被跟踪",[252,305,306],{},[198,307,308],{},"@Published",[252,310,311],{},"对象自己",[179,313,314,317,318,321,322,325],{},[198,315,316],{},"View"," 是结构体，",[198,319,320],{},"body"," 会反复求值。普通 ",[198,323,324],{},"var count = 0"," 每次求值都是一份新的。包装器把存储放在结构体外面，跟这块视图的身份走。",[327,328,256],"h3",{"id":329},"state",[179,331,332,333,335,336,339,340,343,344,346,347,350,351,353],{},"一个开关、一次输入、这一页的计数，用 ",[198,334,256],{},"。",[198,337,338],{},"private"," 表示这份值由这块视图，以及它交出的绑定去改。初始化表达式每次创建视图值时都会执行，SwiftUI 只采纳第一次的结果。能算出来的值做成计算属性，例如 ",[198,341,342],{},"var canSubmit: Bool { !name.isEmpty }","。再存一个 ",[198,345,256],{}," 用 ",[198,348,349],{},"onChange"," 去同步，两份会分叉。",[198,352,349],{}," 留给「值变了要做一件额外的事」，比如打点。",[215,355,360],{"className":356,"code":357,"filename":358,"language":359,"meta":221,"style":221},"language-swift shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import SwiftUI\n\nstruct CounterView: View {\n    \u002F\u002F 方式：@State\n    \u002F\u002F 源在这块视图自己身上。View 是结构体，body 会反复执行；\n    \u002F\u002F 存储跟视图身份走，所以重算之后 count 还是原来的数。\n    @State private var count = 0\n\n    var body: some View {\n        Button(\"\\(count)\") {\n            count += 1 \u002F\u002F 事件里只改 @State，读过 count 的 body 会重算\n        }\n    }\n}\n","CounterView.swift","swift",[198,361,362,375,382,402,409,415,421,447,452,468,497,512,518,524],{"__ignoreMap":221},[363,364,367,371],"span",{"class":365,"line":366},"line",1,[363,368,370],{"class":369},"s7zQu","import",[363,372,374],{"class":373},"sBMFI"," SwiftUI\n",[363,376,378],{"class":365,"line":377},2,[363,379,381],{"emptyLinePlaceholder":380},true,"\n",[363,383,385,389,392,396,399],{"class":365,"line":384},3,[363,386,388],{"class":387},"spNyl","struct",[363,390,391],{"class":373}," CounterView",[363,393,395],{"class":394},"sMK4o",":",[363,397,398],{"class":373}," View ",[363,400,401],{"class":394},"{\n",[363,403,405],{"class":365,"line":404},4,[363,406,408],{"class":407},"sHwdD","    \u002F\u002F 方式：@State\n",[363,410,412],{"class":365,"line":411},5,[363,413,414],{"class":407},"    \u002F\u002F 源在这块视图自己身上。View 是结构体，body 会反复执行；\n",[363,416,418],{"class":365,"line":417},6,[363,419,420],{"class":407},"    \u002F\u002F 存储跟视图身份走，所以重算之后 count 还是原来的数。\n",[363,422,424,427,430,434,437,441,444],{"class":365,"line":423},7,[363,425,426],{"class":394},"    @",[363,428,429],{"class":387},"State",[363,431,433],{"class":432},"sbssI"," private",[363,435,436],{"class":432}," var",[363,438,440],{"class":439},"sTEyZ"," count ",[363,442,443],{"class":394},"=",[363,445,446],{"class":432}," 0\n",[363,448,450],{"class":365,"line":449},8,[363,451,381],{"emptyLinePlaceholder":380},[363,453,455,458,461,464,466],{"class":365,"line":454},9,[363,456,457],{"class":432},"    var",[363,459,460],{"class":439}," body: ",[363,462,463],{"class":432},"some",[363,465,398],{"class":439},[363,467,401],{"class":394},[363,469,471,475,478,481,484,487,490,492,494],{"class":365,"line":470},10,[363,472,474],{"class":473},"s2Zo4","        Button",[363,476,477],{"class":394},"(",[363,479,480],{"class":394},"\"",[363,482,483],{"class":394},"\\(",[363,485,486],{"class":439},"count",[363,488,489],{"class":394},")",[363,491,480],{"class":394},[363,493,489],{"class":394},[363,495,496],{"class":394}," {\n",[363,498,500,503,506,509],{"class":365,"line":499},11,[363,501,502],{"class":439},"            count ",[363,504,505],{"class":394},"+=",[363,507,508],{"class":432}," 1",[363,510,511],{"class":407}," \u002F\u002F 事件里只改 @State，读过 count 的 body 会重算\n",[363,513,515],{"class":365,"line":514},12,[363,516,517],{"class":394},"        }\n",[363,519,521],{"class":365,"line":520},13,[363,522,523],{"class":394},"    }\n",[363,525,527],{"class":365,"line":526},14,[363,528,529],{"class":394},"}\n",[327,531,533],{"id":532},"binding","@Binding",[179,535,536,537,539,540,335,543,546,547,549],{},"子视图要改父视图的值时，用 ",[198,538,533],{},"。传 ",[198,541,542],{},"$count",[198,544,545],{},"$"," 交出的是绑定，子视图写的是同一份存储。子视图里再声明一份 ",[198,548,256],{},"，按钮能加，父视图那个数字不动。",[215,551,554],{"className":356,"code":552,"filename":553,"language":359,"meta":221,"style":221},"import SwiftUI\n\nstruct CounterLabel: View {\n    \u002F\u002F 方式：@Binding\n    \u002F\u002F 子视图不持有存储。这里的 count 就是父视图那份 @State。\n    @Binding var count: Int\n\n    var body: some View {\n        Button(\"\\(count)\") { count += 1 }\n    }\n}\n\nstruct CounterScreen: View {\n    \u002F\u002F 方式：@State，源在父视图\n    @State private var count = 0\n\n    var body: some View {\n        \u002F\u002F $count 的类型是 Binding\u003CInt>，子视图因此能写回这一份\n        CounterLabel(count: $count)\n    }\n}\n","CounterLabel.swift",[198,555,556,562,566,579,584,589,604,608,620,650,654,658,662,675,680,697,702,715,721,739,744],{"__ignoreMap":221},[363,557,558,560],{"class":365,"line":366},[363,559,370],{"class":369},[363,561,374],{"class":373},[363,563,564],{"class":365,"line":377},[363,565,381],{"emptyLinePlaceholder":380},[363,567,568,570,573,575,577],{"class":365,"line":384},[363,569,388],{"class":387},[363,571,572],{"class":373}," CounterLabel",[363,574,395],{"class":394},[363,576,398],{"class":373},[363,578,401],{"class":394},[363,580,581],{"class":365,"line":404},[363,582,583],{"class":407},"    \u002F\u002F 方式：@Binding\n",[363,585,586],{"class":365,"line":411},[363,587,588],{"class":407},"    \u002F\u002F 子视图不持有存储。这里的 count 就是父视图那份 @State。\n",[363,590,591,593,596,598,601],{"class":365,"line":417},[363,592,426],{"class":394},[363,594,595],{"class":387},"Binding",[363,597,436],{"class":432},[363,599,600],{"class":439}," count: ",[363,602,603],{"class":373},"Int\n",[363,605,606],{"class":365,"line":423},[363,607,381],{"emptyLinePlaceholder":380},[363,609,610,612,614,616,618],{"class":365,"line":449},[363,611,457],{"class":432},[363,613,460],{"class":439},[363,615,463],{"class":432},[363,617,398],{"class":439},[363,619,401],{"class":394},[363,621,622,624,626,628,630,632,634,636,638,641,643,645,647],{"class":365,"line":454},[363,623,474],{"class":473},[363,625,477],{"class":394},[363,627,480],{"class":394},[363,629,483],{"class":394},[363,631,486],{"class":439},[363,633,489],{"class":394},[363,635,480],{"class":394},[363,637,489],{"class":394},[363,639,640],{"class":394}," {",[363,642,440],{"class":439},[363,644,505],{"class":394},[363,646,508],{"class":432},[363,648,649],{"class":394}," }\n",[363,651,652],{"class":365,"line":470},[363,653,523],{"class":394},[363,655,656],{"class":365,"line":499},[363,657,529],{"class":394},[363,659,660],{"class":365,"line":514},[363,661,381],{"emptyLinePlaceholder":380},[363,663,664,666,669,671,673],{"class":365,"line":520},[363,665,388],{"class":387},[363,667,668],{"class":373}," CounterScreen",[363,670,395],{"class":394},[363,672,398],{"class":373},[363,674,401],{"class":394},[363,676,677],{"class":365,"line":526},[363,678,679],{"class":407},"    \u002F\u002F 方式：@State，源在父视图\n",[363,681,683,685,687,689,691,693,695],{"class":365,"line":682},15,[363,684,426],{"class":394},[363,686,429],{"class":387},[363,688,433],{"class":432},[363,690,436],{"class":432},[363,692,440],{"class":439},[363,694,443],{"class":394},[363,696,446],{"class":432},[363,698,700],{"class":365,"line":699},16,[363,701,381],{"emptyLinePlaceholder":380},[363,703,705,707,709,711,713],{"class":365,"line":704},17,[363,706,457],{"class":432},[363,708,460],{"class":439},[363,710,463],{"class":432},[363,712,398],{"class":439},[363,714,401],{"class":394},[363,716,718],{"class":365,"line":717},18,[363,719,720],{"class":407},"        \u002F\u002F $count 的类型是 Binding\u003CInt>，子视图因此能写回这一份\n",[363,722,724,727,729,731,733,736],{"class":365,"line":723},19,[363,725,726],{"class":473},"        CounterLabel",[363,728,477],{"class":394},[363,730,486],{"class":473},[363,732,395],{"class":394},[363,734,735],{"class":439}," $count",[363,737,738],{"class":394},")\n",[363,740,742],{"class":365,"line":741},20,[363,743,523],{"class":394},[363,745,747],{"class":365,"line":746},21,[363,748,529],{"class":394},[327,750,200],{"id":751},"observable",[179,753,754,755,757,758,760,761,763,764,767],{},"字段和方法多起来，把这一屏做成类，用 ",[198,756,200],{},"。这是 iOS 17 的对象观察。创建它的视图再用 ",[198,759,256],{}," 拿着实例，重算之后还是这一个对象。",[198,762,320],{}," 读过哪些属性，就跟踪哪些。",[198,765,766],{},"@MainActor"," 把改属性限制在主线程，SwiftUI 按这次写入去刷新界面。",[179,769,770,771,774,775,777,778,780],{},"子视图只显示时，参数用普通的 ",[198,772,773],{},"let","，仍是同一份 ",[198,776,200],{},"。它在 ",[198,779,320],{}," 里读到的属性会被跟踪。",[215,782,785],{"className":356,"code":783,"filename":784,"language":359,"meta":221,"style":221},"import SwiftUI\n\n@MainActor\n@Observable \u002F\u002F 方式：Observation。属性变化会通知读过它的视图\nfinal class CounterModel {\n    private(set) var count = 0 \u002F\u002F 外面只能读，写入走 increment()\n\n    func increment() {\n        count += 1\n    }\n}\n\nstruct CounterModelView: View {\n    \u002F\u002F 方式：@State 持有 @Observable\n    \u002F\u002F 这里存的是类实例。视图重算不会换一个新的 CounterModel。\n    @State private var model = CounterModel()\n\n    var body: some View {\n        Button(\"\\(model.count)\") {\n            model.increment() \u002F\u002F 改的是模型。body 读过 count，所以会重算\n        }\n    }\n}\n\nstruct CounterReadout: View {\n    \u002F\u002F 方式：把 @Observable 只读传下来\n    \u002F\u002F let 不复制存储。子视图和父视图看的是同一个对象。\n    let model: CounterModel\n\n    var body: some View {\n        Text(\"\\(model.count)\")\n    }\n}\n","CounterModel.swift",[198,786,787,793,797,805,815,828,852,856,869,879,883,887,891,904,909,914,934,938,950,971,984,988,993,998,1003,1017,1023,1029,1038,1043,1056,1076,1081],{"__ignoreMap":221},[363,788,789,791],{"class":365,"line":366},[363,790,370],{"class":369},[363,792,374],{"class":373},[363,794,795],{"class":365,"line":377},[363,796,381],{"emptyLinePlaceholder":380},[363,798,799,802],{"class":365,"line":384},[363,800,801],{"class":394},"@",[363,803,804],{"class":387},"MainActor\n",[363,806,807,809,812],{"class":365,"line":404},[363,808,801],{"class":394},[363,810,811],{"class":387},"Observable",[363,813,814],{"class":407}," \u002F\u002F 方式：Observation。属性变化会通知读过它的视图\n",[363,816,817,820,823,826],{"class":365,"line":411},[363,818,819],{"class":387},"final",[363,821,822],{"class":387}," class",[363,824,825],{"class":373}," CounterModel",[363,827,496],{"class":394},[363,829,830,833,835,838,840,842,844,846,849],{"class":365,"line":417},[363,831,832],{"class":432},"    private",[363,834,477],{"class":394},[363,836,837],{"class":387},"set",[363,839,489],{"class":394},[363,841,436],{"class":432},[363,843,440],{"class":439},[363,845,443],{"class":394},[363,847,848],{"class":432}," 0",[363,850,851],{"class":407}," \u002F\u002F 外面只能读，写入走 increment()\n",[363,853,854],{"class":365,"line":423},[363,855,381],{"emptyLinePlaceholder":380},[363,857,858,861,864,867],{"class":365,"line":449},[363,859,860],{"class":387},"    func",[363,862,863],{"class":473}," increment",[363,865,866],{"class":394},"()",[363,868,496],{"class":394},[363,870,871,874,876],{"class":365,"line":454},[363,872,873],{"class":439},"        count ",[363,875,505],{"class":394},[363,877,878],{"class":432}," 1\n",[363,880,881],{"class":365,"line":470},[363,882,523],{"class":394},[363,884,885],{"class":365,"line":499},[363,886,529],{"class":394},[363,888,889],{"class":365,"line":514},[363,890,381],{"emptyLinePlaceholder":380},[363,892,893,895,898,900,902],{"class":365,"line":520},[363,894,388],{"class":387},[363,896,897],{"class":373}," CounterModelView",[363,899,395],{"class":394},[363,901,398],{"class":373},[363,903,401],{"class":394},[363,905,906],{"class":365,"line":526},[363,907,908],{"class":407},"    \u002F\u002F 方式：@State 持有 @Observable\n",[363,910,911],{"class":365,"line":682},[363,912,913],{"class":407},"    \u002F\u002F 这里存的是类实例。视图重算不会换一个新的 CounterModel。\n",[363,915,916,918,920,922,924,927,929,931],{"class":365,"line":699},[363,917,426],{"class":394},[363,919,429],{"class":387},[363,921,433],{"class":432},[363,923,436],{"class":432},[363,925,926],{"class":439}," model ",[363,928,443],{"class":394},[363,930,825],{"class":473},[363,932,933],{"class":394},"()\n",[363,935,936],{"class":365,"line":704},[363,937,381],{"emptyLinePlaceholder":380},[363,939,940,942,944,946,948],{"class":365,"line":717},[363,941,457],{"class":432},[363,943,460],{"class":439},[363,945,463],{"class":432},[363,947,398],{"class":439},[363,949,401],{"class":394},[363,951,952,954,956,958,960,963,965,967,969],{"class":365,"line":723},[363,953,474],{"class":473},[363,955,477],{"class":394},[363,957,480],{"class":394},[363,959,483],{"class":394},[363,961,962],{"class":439},"model.count",[363,964,489],{"class":394},[363,966,480],{"class":394},[363,968,489],{"class":394},[363,970,496],{"class":394},[363,972,973,976,979,981],{"class":365,"line":741},[363,974,975],{"class":439},"            model.",[363,977,978],{"class":473},"increment",[363,980,866],{"class":394},[363,982,983],{"class":407}," \u002F\u002F 改的是模型。body 读过 count，所以会重算\n",[363,985,986],{"class":365,"line":746},[363,987,517],{"class":394},[363,989,991],{"class":365,"line":990},22,[363,992,523],{"class":394},[363,994,996],{"class":365,"line":995},23,[363,997,529],{"class":394},[363,999,1001],{"class":365,"line":1000},24,[363,1002,381],{"emptyLinePlaceholder":380},[363,1004,1006,1008,1011,1013,1015],{"class":365,"line":1005},25,[363,1007,388],{"class":387},[363,1009,1010],{"class":373}," CounterReadout",[363,1012,395],{"class":394},[363,1014,398],{"class":373},[363,1016,401],{"class":394},[363,1018,1020],{"class":365,"line":1019},26,[363,1021,1022],{"class":407},"    \u002F\u002F 方式：把 @Observable 只读传下来\n",[363,1024,1026],{"class":365,"line":1025},27,[363,1027,1028],{"class":407},"    \u002F\u002F let 不复制存储。子视图和父视图看的是同一个对象。\n",[363,1030,1032,1035],{"class":365,"line":1031},28,[363,1033,1034],{"class":432},"    let",[363,1036,1037],{"class":439}," model: CounterModel\n",[363,1039,1041],{"class":365,"line":1040},29,[363,1042,381],{"emptyLinePlaceholder":380},[363,1044,1046,1048,1050,1052,1054],{"class":365,"line":1045},30,[363,1047,457],{"class":432},[363,1049,460],{"class":439},[363,1051,463],{"class":432},[363,1053,398],{"class":439},[363,1055,401],{"class":394},[363,1057,1059,1062,1064,1066,1068,1070,1072,1074],{"class":365,"line":1058},31,[363,1060,1061],{"class":473},"        Text",[363,1063,477],{"class":394},[363,1065,480],{"class":394},[363,1067,483],{"class":394},[363,1069,962],{"class":439},[363,1071,489],{"class":394},[363,1073,480],{"class":394},[363,1075,738],{"class":394},[363,1077,1079],{"class":365,"line":1078},32,[363,1080,523],{"class":394},[363,1082,1084],{"class":365,"line":1083},33,[363,1085,529],{"class":394},[327,1087,275],{"id":1088},"bindable",[179,1090,1091,1092,1095,1096,1098,1099,1101,1102,1105,1106,1108,1109,1111],{},"子视图要交给 ",[198,1093,1094],{},"TextField"," 这种需要绑定的控件时，用 ",[198,1097,275],{},"。它不创建对象，只让外面传进来的 ",[198,1100,200],{}," 能写出 ",[198,1103,1104],{},"$model.name","。创建那一块已经有 ",[198,1107,256],{}," 时，那里的 ",[198,1110,1104],{}," 可以直接用，不必再套一层。",[215,1113,1116],{"className":356,"code":1114,"filename":1115,"language":359,"meta":221,"style":221},"import SwiftUI\n\n@MainActor\n@Observable\nfinal class ProfileModel {\n    var name = \"\"\n}\n\nstruct NameField: View {\n    \u002F\u002F 方式：@Bindable\n    \u002F\u002F 对象是父视图持有的。@Bindable 只负责把 name 投影成 Binding\u003CString>。\n    @Bindable var model: ProfileModel\n\n    var body: some View {\n        TextField(\"名字\", text: $model.name) \u002F\u002F $model.name 写回 ProfileModel.name\n    }\n}\n","NameField.swift",[198,1117,1118,1124,1128,1134,1141,1152,1164,1168,1172,1185,1190,1195,1207,1211,1223,1253,1257],{"__ignoreMap":221},[363,1119,1120,1122],{"class":365,"line":366},[363,1121,370],{"class":369},[363,1123,374],{"class":373},[363,1125,1126],{"class":365,"line":377},[363,1127,381],{"emptyLinePlaceholder":380},[363,1129,1130,1132],{"class":365,"line":384},[363,1131,801],{"class":394},[363,1133,804],{"class":387},[363,1135,1136,1138],{"class":365,"line":404},[363,1137,801],{"class":394},[363,1139,1140],{"class":387},"Observable\n",[363,1142,1143,1145,1147,1150],{"class":365,"line":411},[363,1144,819],{"class":387},[363,1146,822],{"class":387},[363,1148,1149],{"class":373}," ProfileModel",[363,1151,496],{"class":394},[363,1153,1154,1156,1159,1161],{"class":365,"line":417},[363,1155,457],{"class":432},[363,1157,1158],{"class":439}," name ",[363,1160,443],{"class":394},[363,1162,1163],{"class":394}," \"\"\n",[363,1165,1166],{"class":365,"line":423},[363,1167,529],{"class":394},[363,1169,1170],{"class":365,"line":449},[363,1171,381],{"emptyLinePlaceholder":380},[363,1173,1174,1176,1179,1181,1183],{"class":365,"line":454},[363,1175,388],{"class":387},[363,1177,1178],{"class":373}," NameField",[363,1180,395],{"class":394},[363,1182,398],{"class":373},[363,1184,401],{"class":394},[363,1186,1187],{"class":365,"line":470},[363,1188,1189],{"class":407},"    \u002F\u002F 方式：@Bindable\n",[363,1191,1192],{"class":365,"line":499},[363,1193,1194],{"class":407},"    \u002F\u002F 对象是父视图持有的。@Bindable 只负责把 name 投影成 Binding\u003CString>。\n",[363,1196,1197,1199,1202,1204],{"class":365,"line":514},[363,1198,426],{"class":394},[363,1200,1201],{"class":387},"Bindable",[363,1203,436],{"class":432},[363,1205,1206],{"class":439}," model: ProfileModel\n",[363,1208,1209],{"class":365,"line":520},[363,1210,381],{"emptyLinePlaceholder":380},[363,1212,1213,1215,1217,1219,1221],{"class":365,"line":526},[363,1214,457],{"class":432},[363,1216,460],{"class":439},[363,1218,463],{"class":432},[363,1220,398],{"class":439},[363,1222,401],{"class":394},[363,1224,1225,1228,1230,1232,1236,1238,1241,1243,1245,1248,1250],{"class":365,"line":682},[363,1226,1227],{"class":473},"        TextField",[363,1229,477],{"class":394},[363,1231,480],{"class":394},[363,1233,1235],{"class":1234},"sfazB","名字",[363,1237,480],{"class":394},[363,1239,1240],{"class":439},", ",[363,1242,220],{"class":473},[363,1244,395],{"class":394},[363,1246,1247],{"class":439}," $model.name",[363,1249,489],{"class":394},[363,1251,1252],{"class":407}," \u002F\u002F $model.name 写回 ProfileModel.name\n",[363,1254,1255],{"class":365,"line":699},[363,1256,523],{"class":394},[363,1258,1259],{"class":365,"line":704},[363,1260,529],{"class":394},[327,1262,290],{"id":1263},"environment",[179,1265,1266,1267,1269,1270,1273,1274,1276,1277,1279,1280,1283,1284,1287,1288,1290,1291,335],{},"很多层要同一份模型时，用环境。根上用 ",[198,1268,256],{}," 创建一次，",[198,1271,1272],{},".environment(model)"," 放进去。子视图用 ",[198,1275,290],{}," 取出。每一层再 ",[198,1278,256],{}," 一份，就是多份计数。根上忘了注入，子视图一读就会崩。可以写成 ",[198,1281,1282],{},"@Environment(CounterModel.self) private var model: CounterModel?","，没有注入时是 ",[198,1285,1286],{},"nil","。从环境取出之后还要做绑定，在 ",[198,1289,320],{}," 里再取一次：",[198,1292,1293],{},"@Bindable var model = model",[179,1295,1296,1297,1300,1301,1303],{},"旧代码是 ",[198,1298,1299],{},".environmentObject(model)"," 和 ",[198,1302,295],{},"。两套键不一样，注入和读取用同一套。",[215,1305,1308],{"className":356,"code":1306,"filename":1307,"language":359,"meta":221,"style":221},"import SwiftUI\n\nstruct CounterRootView: View {\n    \u002F\u002F 方式：@State 持有，全树只创建这一次\n    @State private var model = CounterModel()\n\n    var body: some View {\n        CounterDetailView()\n            .environment(model) \u002F\u002F 方式：环境注入。子视图不用逐层写参数\n    }\n}\n\nstruct CounterDetailView: View {\n    \u002F\u002F 方式：@Environment\n    \u002F\u002F 从祖先注入的那一份 @Observable 里取。读到的属性仍会触发重算。\n    @Environment(CounterModel.self) private var model\n\n    var body: some View {\n        Text(\"\\(model.count)\")\n    }\n}\n","CounterRootView.swift",[198,1309,1310,1316,1320,1333,1338,1356,1360,1372,1379,1396,1400,1404,1408,1421,1426,1431,1455,1459,1471,1489,1493],{"__ignoreMap":221},[363,1311,1312,1314],{"class":365,"line":366},[363,1313,370],{"class":369},[363,1315,374],{"class":373},[363,1317,1318],{"class":365,"line":377},[363,1319,381],{"emptyLinePlaceholder":380},[363,1321,1322,1324,1327,1329,1331],{"class":365,"line":384},[363,1323,388],{"class":387},[363,1325,1326],{"class":373}," CounterRootView",[363,1328,395],{"class":394},[363,1330,398],{"class":373},[363,1332,401],{"class":394},[363,1334,1335],{"class":365,"line":404},[363,1336,1337],{"class":407},"    \u002F\u002F 方式：@State 持有，全树只创建这一次\n",[363,1339,1340,1342,1344,1346,1348,1350,1352,1354],{"class":365,"line":411},[363,1341,426],{"class":394},[363,1343,429],{"class":387},[363,1345,433],{"class":432},[363,1347,436],{"class":432},[363,1349,926],{"class":439},[363,1351,443],{"class":394},[363,1353,825],{"class":473},[363,1355,933],{"class":394},[363,1357,1358],{"class":365,"line":417},[363,1359,381],{"emptyLinePlaceholder":380},[363,1361,1362,1364,1366,1368,1370],{"class":365,"line":423},[363,1363,457],{"class":432},[363,1365,460],{"class":439},[363,1367,463],{"class":432},[363,1369,398],{"class":439},[363,1371,401],{"class":394},[363,1373,1374,1377],{"class":365,"line":449},[363,1375,1376],{"class":473},"        CounterDetailView",[363,1378,933],{"class":394},[363,1380,1381,1384,1386,1388,1391,1393],{"class":365,"line":454},[363,1382,1383],{"class":439},"            .",[363,1385,1263],{"class":473},[363,1387,477],{"class":394},[363,1389,1390],{"class":439},"model",[363,1392,489],{"class":394},[363,1394,1395],{"class":407}," \u002F\u002F 方式：环境注入。子视图不用逐层写参数\n",[363,1397,1398],{"class":365,"line":470},[363,1399,523],{"class":394},[363,1401,1402],{"class":365,"line":499},[363,1403,529],{"class":394},[363,1405,1406],{"class":365,"line":514},[363,1407,381],{"emptyLinePlaceholder":380},[363,1409,1410,1412,1415,1417,1419],{"class":365,"line":520},[363,1411,388],{"class":387},[363,1413,1414],{"class":373}," CounterDetailView",[363,1416,395],{"class":394},[363,1418,398],{"class":373},[363,1420,401],{"class":394},[363,1422,1423],{"class":365,"line":526},[363,1424,1425],{"class":407},"    \u002F\u002F 方式：@Environment\n",[363,1427,1428],{"class":365,"line":682},[363,1429,1430],{"class":407},"    \u002F\u002F 从祖先注入的那一份 @Observable 里取。读到的属性仍会触发重算。\n",[363,1432,1433,1435,1438,1440,1443,1446,1448,1450,1452],{"class":365,"line":699},[363,1434,426],{"class":394},[363,1436,1437],{"class":387},"Environment",[363,1439,477],{"class":394},[363,1441,1442],{"class":439},"CounterModel.",[363,1444,1445],{"class":432},"self",[363,1447,489],{"class":394},[363,1449,433],{"class":432},[363,1451,436],{"class":432},[363,1453,1454],{"class":439}," model\n",[363,1456,1457],{"class":365,"line":704},[363,1458,381],{"emptyLinePlaceholder":380},[363,1460,1461,1463,1465,1467,1469],{"class":365,"line":717},[363,1462,457],{"class":432},[363,1464,460],{"class":439},[363,1466,463],{"class":432},[363,1468,398],{"class":439},[363,1470,401],{"class":394},[363,1472,1473,1475,1477,1479,1481,1483,1485,1487],{"class":365,"line":723},[363,1474,1061],{"class":473},[363,1476,477],{"class":394},[363,1478,480],{"class":394},[363,1480,483],{"class":394},[363,1482,962],{"class":439},[363,1484,489],{"class":394},[363,1486,480],{"class":394},[363,1488,738],{"class":394},[363,1490,1491],{"class":365,"line":741},[363,1492,523],{"class":394},[363,1494,1495],{"class":365,"line":746},[363,1496,529],{"class":394},[327,1498,264],{"id":1499},"stateobject",[179,1501,1502,1503,1505,1506,1508,1509,1511,1512,1514,1515,1517,1518,1520],{},"iOS 17 之前用 ",[198,1504,204],{},"。要带动界面的字段标 ",[198,1507,308],{},"。创建并持有用 ",[198,1510,264],{},"，外面传进来用 ",[198,1513,280],{},"。任意一个 ",[198,1516,308],{}," 变化，依赖这个对象的视图都会重算，包括 ",[198,1519,320],{}," 没读的字段。",[215,1522,1525],{"className":356,"code":1523,"filename":1524,"language":359,"meta":221,"style":221},"import Combine\nimport SwiftUI\n\nfinal class LegacyCounterModel: ObservableObject {\n    \u002F\u002F 方式：@Published。赋值时通过 ObservableObject 通知界面\n    @Published private(set) var count = 0\n\n    func increment() {\n        count += 1\n    }\n}\n\nstruct LegacyCounterView: View {\n    \u002F\u002F 方式：@StateObject\n    \u002F\u002F 由这块视图创建并持有。对象活在视图身份上，body 重算不会换新实例。\n    @StateObject private var model = LegacyCounterModel()\n\n    var body: some View {\n        Button(\"\\(model.count)\") {\n            model.increment()\n        }\n    }\n}\n","LegacyCounterModel.swift",[198,1526,1527,1534,1540,1544,1560,1565,1588,1592,1602,1610,1614,1618,1622,1635,1640,1645,1664,1668,1680,1700,1708,1712,1716],{"__ignoreMap":221},[363,1528,1529,1531],{"class":365,"line":366},[363,1530,370],{"class":369},[363,1532,1533],{"class":373}," Combine\n",[363,1535,1536,1538],{"class":365,"line":377},[363,1537,370],{"class":369},[363,1539,374],{"class":373},[363,1541,1542],{"class":365,"line":384},[363,1543,381],{"emptyLinePlaceholder":380},[363,1545,1546,1548,1550,1553,1555,1558],{"class":365,"line":404},[363,1547,819],{"class":387},[363,1549,822],{"class":387},[363,1551,1552],{"class":373}," LegacyCounterModel",[363,1554,395],{"class":394},[363,1556,1557],{"class":373}," ObservableObject ",[363,1559,401],{"class":394},[363,1561,1562],{"class":365,"line":411},[363,1563,1564],{"class":407},"    \u002F\u002F 方式：@Published。赋值时通过 ObservableObject 通知界面\n",[363,1566,1567,1569,1572,1574,1576,1578,1580,1582,1584,1586],{"class":365,"line":417},[363,1568,426],{"class":394},[363,1570,1571],{"class":387},"Published",[363,1573,433],{"class":432},[363,1575,477],{"class":394},[363,1577,837],{"class":387},[363,1579,489],{"class":394},[363,1581,436],{"class":432},[363,1583,440],{"class":439},[363,1585,443],{"class":394},[363,1587,446],{"class":432},[363,1589,1590],{"class":365,"line":423},[363,1591,381],{"emptyLinePlaceholder":380},[363,1593,1594,1596,1598,1600],{"class":365,"line":449},[363,1595,860],{"class":387},[363,1597,863],{"class":473},[363,1599,866],{"class":394},[363,1601,496],{"class":394},[363,1603,1604,1606,1608],{"class":365,"line":454},[363,1605,873],{"class":439},[363,1607,505],{"class":394},[363,1609,878],{"class":432},[363,1611,1612],{"class":365,"line":470},[363,1613,523],{"class":394},[363,1615,1616],{"class":365,"line":499},[363,1617,529],{"class":394},[363,1619,1620],{"class":365,"line":514},[363,1621,381],{"emptyLinePlaceholder":380},[363,1623,1624,1626,1629,1631,1633],{"class":365,"line":520},[363,1625,388],{"class":387},[363,1627,1628],{"class":373}," LegacyCounterView",[363,1630,395],{"class":394},[363,1632,398],{"class":373},[363,1634,401],{"class":394},[363,1636,1637],{"class":365,"line":526},[363,1638,1639],{"class":407},"    \u002F\u002F 方式：@StateObject\n",[363,1641,1642],{"class":365,"line":682},[363,1643,1644],{"class":407},"    \u002F\u002F 由这块视图创建并持有。对象活在视图身份上，body 重算不会换新实例。\n",[363,1646,1647,1649,1652,1654,1656,1658,1660,1662],{"class":365,"line":699},[363,1648,426],{"class":394},[363,1650,1651],{"class":387},"StateObject",[363,1653,433],{"class":432},[363,1655,436],{"class":432},[363,1657,926],{"class":439},[363,1659,443],{"class":394},[363,1661,1552],{"class":473},[363,1663,933],{"class":394},[363,1665,1666],{"class":365,"line":704},[363,1667,381],{"emptyLinePlaceholder":380},[363,1669,1670,1672,1674,1676,1678],{"class":365,"line":717},[363,1671,457],{"class":432},[363,1673,460],{"class":439},[363,1675,463],{"class":432},[363,1677,398],{"class":439},[363,1679,401],{"class":394},[363,1681,1682,1684,1686,1688,1690,1692,1694,1696,1698],{"class":365,"line":723},[363,1683,474],{"class":473},[363,1685,477],{"class":394},[363,1687,480],{"class":394},[363,1689,483],{"class":394},[363,1691,962],{"class":439},[363,1693,489],{"class":394},[363,1695,480],{"class":394},[363,1697,489],{"class":394},[363,1699,496],{"class":394},[363,1701,1702,1704,1706],{"class":365,"line":741},[363,1703,975],{"class":439},[363,1705,978],{"class":473},[363,1707,933],{"class":394},[363,1709,1710],{"class":365,"line":746},[363,1711,517],{"class":394},[363,1713,1714],{"class":365,"line":990},[363,1715,523],{"class":394},[363,1717,1718],{"class":365,"line":995},[363,1719,529],{"class":394},[1721,1722,1723,1726,1727,1729,1730,335],"note",{},[198,1724,1725],{},"@ObservedObject private var model = LegacyCounterModel()"," 的默认值会在视图每次初始化时执行。父视图重算、这块视图再次初始化，计数回到 0。创建并持有，用 ",[198,1728,264],{},"。对象是参数传进来的，才用 ",[198,1731,280],{},[179,1733,1734,1737,1738,1741,1742,1745,1746,1749,1750,1749,1753,1749,1756,1749,1759,1749,1762,1765,1766,1769,1770,1773,1774,1777,1778,1781,1782,1785,1786,1789],{},[198,1735,1736],{},"@AppStorage"," 读写 ",[198,1739,1740],{},"UserDefaults","，适合通知开关、外观这种偏好。",[198,1743,1744],{},"@SceneStorage"," 跟着这个场景走，适合选中的标签、滚动位置。键是字符串，类型是包装器支持的那几种（",[198,1747,1748],{},"Bool","、",[198,1751,1752],{},"Int",[198,1754,1755],{},"Double",[198,1757,1758],{},"String",[198,1760,1761],{},"URL",[198,1763,1764],{},"Data","，以及 ",[198,1767,1768],{},"RawRepresentable","）。",[198,1771,1772],{},"$notificationsOn"," 同样是绑定，可以传给子视图。列表、账号、网络结果放在页面模型里。SwiftData 的 ",[198,1775,1776],{},"@Query"," 是查询结果，写入走 ",[198,1779,1780],{},"ModelContext","，和这架梯子分开。",[198,1783,1784],{},"@FocusState"," 管焦点。",[198,1787,1788],{},"@GestureState"," 只活在一次手势里，手势结束回到初值。",[215,1791,1794],{"className":356,"code":1792,"filename":1793,"language":359,"meta":221,"style":221},"import SwiftUI\n\nstruct SettingsView: View {\n    \u002F\u002F 方式：@AppStorage\n    \u002F\u002F 源在 UserDefaults，键是 \"notificationsOn\"。视图重算之后开关状态还在。\n    @AppStorage(\"notificationsOn\") private var notificationsOn = false\n\n    var body: some View {\n        Toggle(\"通知\", isOn: $notificationsOn) \u002F\u002F $ 得到 Binding，和 @State 传给子视图相同\n    }\n}\n","SettingsView.swift",[198,1795,1796,1802,1806,1819,1824,1829,1860,1864,1876,1905,1909],{"__ignoreMap":221},[363,1797,1798,1800],{"class":365,"line":366},[363,1799,370],{"class":369},[363,1801,374],{"class":373},[363,1803,1804],{"class":365,"line":377},[363,1805,381],{"emptyLinePlaceholder":380},[363,1807,1808,1810,1813,1815,1817],{"class":365,"line":384},[363,1809,388],{"class":387},[363,1811,1812],{"class":373}," SettingsView",[363,1814,395],{"class":394},[363,1816,398],{"class":373},[363,1818,401],{"class":394},[363,1820,1821],{"class":365,"line":404},[363,1822,1823],{"class":407},"    \u002F\u002F 方式：@AppStorage\n",[363,1825,1826],{"class":365,"line":411},[363,1827,1828],{"class":407},"    \u002F\u002F 源在 UserDefaults，键是 \"notificationsOn\"。视图重算之后开关状态还在。\n",[363,1830,1831,1833,1836,1838,1840,1843,1845,1847,1849,1851,1854,1856],{"class":365,"line":417},[363,1832,426],{"class":394},[363,1834,1835],{"class":387},"AppStorage",[363,1837,477],{"class":394},[363,1839,480],{"class":394},[363,1841,1842],{"class":1234},"notificationsOn",[363,1844,480],{"class":394},[363,1846,489],{"class":394},[363,1848,433],{"class":432},[363,1850,436],{"class":432},[363,1852,1853],{"class":439}," notificationsOn ",[363,1855,443],{"class":394},[363,1857,1859],{"class":1858},"sfNiH"," false\n",[363,1861,1862],{"class":365,"line":423},[363,1863,381],{"emptyLinePlaceholder":380},[363,1865,1866,1868,1870,1872,1874],{"class":365,"line":449},[363,1867,457],{"class":432},[363,1869,460],{"class":439},[363,1871,463],{"class":432},[363,1873,398],{"class":439},[363,1875,401],{"class":394},[363,1877,1878,1881,1883,1885,1888,1890,1892,1895,1897,1900,1902],{"class":365,"line":454},[363,1879,1880],{"class":473},"        Toggle",[363,1882,477],{"class":394},[363,1884,480],{"class":394},[363,1886,1887],{"class":1234},"通知",[363,1889,480],{"class":394},[363,1891,1240],{"class":439},[363,1893,1894],{"class":473},"isOn",[363,1896,395],{"class":394},[363,1898,1899],{"class":439}," $notificationsOn",[363,1901,489],{"class":394},[363,1903,1904],{"class":407}," \u002F\u002F $ 得到 Binding，和 @State 传给子视图相同\n",[363,1906,1907],{"class":365,"line":470},[363,1908,523],{"class":394},[363,1910,1911],{"class":365,"line":499},[363,1912,529],{"class":394},[179,1914,1915],{},"按这个选：",[1917,1918,1919,1928,1945,1955,1963],"ul",{},[1920,1921,1922,1923,1925,1926,335],"li",{},"一个值，一块视图：",[198,1924,256],{},"。子视图要改：",[198,1927,533],{},[1920,1929,1930,1931,1933,1934,1936,1937,1939,1940,346,1943,335],{},"多个字段和动作：",[198,1932,200],{},"。创建处 ",[198,1935,256],{},"，只读下传用 ",[198,1938,773],{},"，要 ",[198,1941,1942],{},"$字段",[198,1944,275],{},[1920,1946,1947,1948,1950,1951,1954],{},"跨很多层：根上 ",[198,1949,256],{}," 创建，",[198,1952,1953],{},".environment"," 传下去。",[1920,1956,1957,1958,1960,1961,335],{},"偏好用 ",[198,1959,1736],{},"，场景恢复用 ",[198,1962,1744],{},[1920,1964,1965,1966,1968,1969,1971,1972,1974,1975,335],{},"目标早于 iOS 17：",[198,1967,204],{},"，创建处 ",[198,1970,264],{},"，传入处 ",[198,1973,280],{},"，子树用 ",[198,1976,295],{},[207,1978,1980],{"id":1979},"uikit-数据流","UIKit 数据流",[179,1982,1983],{},"UIKit 的界面是一棵你拿着的控件。状态变了，没有人替你重算界面。事件进控制器，控制器改模型，再把模型写进控件。中间漏一步，屏幕上的字和数据就分开了。",[215,1985,1988],{"className":1986,"code":1987,"language":220,"meta":221},[218],"点击 \u002F 输入 \u002F 回调\n        │\n        ▼\n  控制器改模型\n        │\n        ▼\n  apply() 写进控件\n",[198,1989,1987],{"__ignoreMap":221},[179,1991,1992,1993,1996,1997,2000],{},"投影只在 ",[198,1994,1995],{},"apply()"," 里写。按钮、通知、网络回调都先改模型，再调用这一个方法。两处各自写 ",[198,1998,1999],{},"label.text","，很快会有一条路径忘了改标题或忘了改按钮。",[327,2002,2003],{"id":2003},"自己刷新",[215,2005,2008],{"className":356,"code":2006,"filename":2007,"language":359,"meta":221,"style":221},"import UIKit\n\nfinal class CounterViewController: UIViewController {\n    private var count = 0 \u002F\u002F 这一页的源\n    private let label = UILabel()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        label.frame = CGRect(x: 20, y: 100, width: 200, height: 40)\n        view.addSubview(label)\n\n        let button = UIButton(type: .system)\n        button.frame = CGRect(x: 20, y: 150, width: 80, height: 40)\n        button.setTitle(\"加一\", for: .normal)\n        button.addTarget(self, action: #selector(increment), for: .touchUpInside)\n        view.addSubview(button)\n        apply() \u002F\u002F 第一次把 0 画上去\n    }\n\n    @objc private func increment() {\n        count += 1\n        apply() \u002F\u002F 改完模型再画，标签只在这里写\n    }\n\n    private func apply() {\n        label.text = \"\\(count)\"\n    }\n}\n","CounterViewController.swift",[198,2009,2010,2017,2021,2037,2052,2069,2073,2088,2098,2150,2165,2169,2194,2239,2268,2305,2318,2328,2332,2336,2353,2361,2370,2374,2378,2391,2410,2414],{"__ignoreMap":221},[363,2011,2012,2014],{"class":365,"line":366},[363,2013,370],{"class":369},[363,2015,2016],{"class":373}," UIKit\n",[363,2018,2019],{"class":365,"line":377},[363,2020,381],{"emptyLinePlaceholder":380},[363,2022,2023,2025,2027,2030,2032,2035],{"class":365,"line":384},[363,2024,819],{"class":387},[363,2026,822],{"class":387},[363,2028,2029],{"class":373}," CounterViewController",[363,2031,395],{"class":394},[363,2033,2034],{"class":373}," UIViewController ",[363,2036,401],{"class":394},[363,2038,2039,2041,2043,2045,2047,2049],{"class":365,"line":404},[363,2040,832],{"class":432},[363,2042,436],{"class":432},[363,2044,440],{"class":439},[363,2046,443],{"class":394},[363,2048,848],{"class":432},[363,2050,2051],{"class":407}," \u002F\u002F 这一页的源\n",[363,2053,2054,2056,2059,2062,2064,2067],{"class":365,"line":411},[363,2055,832],{"class":432},[363,2057,2058],{"class":432}," let",[363,2060,2061],{"class":439}," label ",[363,2063,443],{"class":394},[363,2065,2066],{"class":473}," UILabel",[363,2068,933],{"class":394},[363,2070,2071],{"class":365,"line":417},[363,2072,381],{"emptyLinePlaceholder":380},[363,2074,2075,2078,2081,2084,2086],{"class":365,"line":423},[363,2076,2077],{"class":387},"    override",[363,2079,2080],{"class":387}," func",[363,2082,2083],{"class":473}," viewDidLoad",[363,2085,866],{"class":394},[363,2087,496],{"class":394},[363,2089,2090,2093,2096],{"class":365,"line":449},[363,2091,2092],{"class":439},"        super.",[363,2094,2095],{"class":473},"viewDidLoad",[363,2097,933],{"class":394},[363,2099,2100,2103,2105,2108,2110,2113,2115,2118,2120,2123,2125,2128,2130,2133,2135,2138,2140,2143,2145,2148],{"class":365,"line":454},[363,2101,2102],{"class":439},"        label.frame ",[363,2104,443],{"class":394},[363,2106,2107],{"class":473}," CGRect",[363,2109,477],{"class":394},[363,2111,2112],{"class":473},"x",[363,2114,395],{"class":394},[363,2116,2117],{"class":432}," 20",[363,2119,1240],{"class":439},[363,2121,2122],{"class":473},"y",[363,2124,395],{"class":394},[363,2126,2127],{"class":432}," 100",[363,2129,1240],{"class":439},[363,2131,2132],{"class":473},"width",[363,2134,395],{"class":394},[363,2136,2137],{"class":432}," 200",[363,2139,1240],{"class":439},[363,2141,2142],{"class":473},"height",[363,2144,395],{"class":394},[363,2146,2147],{"class":432}," 40",[363,2149,738],{"class":394},[363,2151,2152,2155,2158,2160,2163],{"class":365,"line":470},[363,2153,2154],{"class":439},"        view.",[363,2156,2157],{"class":473},"addSubview",[363,2159,477],{"class":394},[363,2161,2162],{"class":439},"label",[363,2164,738],{"class":394},[363,2166,2167],{"class":365,"line":499},[363,2168,381],{"emptyLinePlaceholder":380},[363,2170,2171,2174,2177,2179,2182,2184,2187,2189,2192],{"class":365,"line":514},[363,2172,2173],{"class":432},"        let",[363,2175,2176],{"class":439}," button ",[363,2178,443],{"class":394},[363,2180,2181],{"class":473}," UIButton",[363,2183,477],{"class":394},[363,2185,2186],{"class":473},"type",[363,2188,395],{"class":394},[363,2190,2191],{"class":439}," .system",[363,2193,738],{"class":394},[363,2195,2196,2199,2201,2203,2205,2207,2209,2211,2213,2215,2217,2220,2222,2224,2226,2229,2231,2233,2235,2237],{"class":365,"line":520},[363,2197,2198],{"class":439},"        button.frame ",[363,2200,443],{"class":394},[363,2202,2107],{"class":473},[363,2204,477],{"class":394},[363,2206,2112],{"class":473},[363,2208,395],{"class":394},[363,2210,2117],{"class":432},[363,2212,1240],{"class":439},[363,2214,2122],{"class":473},[363,2216,395],{"class":394},[363,2218,2219],{"class":432}," 150",[363,2221,1240],{"class":439},[363,2223,2132],{"class":473},[363,2225,395],{"class":394},[363,2227,2228],{"class":432}," 80",[363,2230,1240],{"class":439},[363,2232,2142],{"class":473},[363,2234,395],{"class":394},[363,2236,2147],{"class":432},[363,2238,738],{"class":394},[363,2240,2241,2244,2247,2249,2251,2254,2256,2258,2261,2263,2266],{"class":365,"line":526},[363,2242,2243],{"class":439},"        button.",[363,2245,2246],{"class":473},"setTitle",[363,2248,477],{"class":394},[363,2250,480],{"class":394},[363,2252,2253],{"class":1234},"加一",[363,2255,480],{"class":394},[363,2257,1240],{"class":439},[363,2259,2260],{"class":473},"for",[363,2262,395],{"class":394},[363,2264,2265],{"class":439}," .normal",[363,2267,738],{"class":394},[363,2269,2270,2272,2275,2277,2280,2283,2285,2288,2290,2292,2294,2296,2298,2300,2303],{"class":365,"line":682},[363,2271,2243],{"class":439},[363,2273,2274],{"class":473},"addTarget",[363,2276,477],{"class":394},[363,2278,2279],{"class":439},"self, ",[363,2281,2282],{"class":473},"action",[363,2284,395],{"class":394},[363,2286,2287],{"class":473}," #selector",[363,2289,477],{"class":394},[363,2291,978],{"class":439},[363,2293,489],{"class":394},[363,2295,1240],{"class":439},[363,2297,2260],{"class":473},[363,2299,395],{"class":394},[363,2301,2302],{"class":439}," .touchUpInside",[363,2304,738],{"class":394},[363,2306,2307,2309,2311,2313,2316],{"class":365,"line":699},[363,2308,2154],{"class":439},[363,2310,2157],{"class":473},[363,2312,477],{"class":394},[363,2314,2315],{"class":439},"button",[363,2317,738],{"class":394},[363,2319,2320,2323,2325],{"class":365,"line":704},[363,2321,2322],{"class":473},"        apply",[363,2324,866],{"class":394},[363,2326,2327],{"class":407}," \u002F\u002F 第一次把 0 画上去\n",[363,2329,2330],{"class":365,"line":717},[363,2331,523],{"class":394},[363,2333,2334],{"class":365,"line":723},[363,2335,381],{"emptyLinePlaceholder":380},[363,2337,2338,2340,2343,2345,2347,2349,2351],{"class":365,"line":741},[363,2339,426],{"class":394},[363,2341,2342],{"class":387},"objc",[363,2344,433],{"class":432},[363,2346,2080],{"class":387},[363,2348,863],{"class":473},[363,2350,866],{"class":394},[363,2352,496],{"class":394},[363,2354,2355,2357,2359],{"class":365,"line":746},[363,2356,873],{"class":439},[363,2358,505],{"class":394},[363,2360,878],{"class":432},[363,2362,2363,2365,2367],{"class":365,"line":990},[363,2364,2322],{"class":473},[363,2366,866],{"class":394},[363,2368,2369],{"class":407}," \u002F\u002F 改完模型再画，标签只在这里写\n",[363,2371,2372],{"class":365,"line":995},[363,2373,523],{"class":394},[363,2375,2376],{"class":365,"line":1000},[363,2377,381],{"emptyLinePlaceholder":380},[363,2379,2380,2382,2384,2387,2389],{"class":365,"line":1005},[363,2381,832],{"class":432},[363,2383,2080],{"class":387},[363,2385,2386],{"class":473}," apply",[363,2388,866],{"class":394},[363,2390,496],{"class":394},[363,2392,2393,2396,2398,2401,2403,2405,2407],{"class":365,"line":1019},[363,2394,2395],{"class":439},"        label.text ",[363,2397,443],{"class":394},[363,2399,2400],{"class":394}," \"",[363,2402,483],{"class":394},[363,2404,486],{"class":439},[363,2406,489],{"class":394},[363,2408,2409],{"class":394},"\"\n",[363,2411,2412],{"class":365,"line":1025},[363,2413,523],{"class":394},[363,2415,2416],{"class":365,"line":1031},[363,2417,529],{"class":394},[179,2419,2420,2422,2423,2425],{},[198,2421,486],{}," 是源，",[198,2424,1999],{}," 是投影。控制器在的时候，这份源就在。",[327,2427,2428],{"id":2428},"向上回调",[179,2430,2431,2432,2434,2435,2437],{},"设置页持有 ",[198,2433,1842],{},"，这一页的标题也要显示开或关，所以值放在设置页。开关那一行只负责把新的 ",[198,2436,1748],{}," 交上来。",[215,2439,2442],{"className":356,"code":2440,"filename":2441,"language":359,"meta":221,"style":221},"import UIKit\n\nfinal class ToggleRow: UIView {\n    let toggle = UISwitch()\n    var onChange: ((Bool) -> Void)? \u002F\u002F 父视图填进来，用来接收新值\n\n    override init(frame: CGRect) {\n        super.init(frame: frame)\n        toggle.addTarget(self, action: #selector(switched), for: .valueChanged)\n        addSubview(toggle)\n    }\n\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    @objc private func switched() {\n        onChange?(toggle.isOn)\n    }\n}\n","ToggleRow.swift",[198,2443,2444,2450,2454,2470,2484,2510,2514,2534,2552,2587,2599,2603,2607,2627,2643,2647,2651,2668,2681,2685],{"__ignoreMap":221},[363,2445,2446,2448],{"class":365,"line":366},[363,2447,370],{"class":369},[363,2449,2016],{"class":373},[363,2451,2452],{"class":365,"line":377},[363,2453,381],{"emptyLinePlaceholder":380},[363,2455,2456,2458,2460,2463,2465,2468],{"class":365,"line":384},[363,2457,819],{"class":387},[363,2459,822],{"class":387},[363,2461,2462],{"class":373}," ToggleRow",[363,2464,395],{"class":394},[363,2466,2467],{"class":373}," UIView ",[363,2469,401],{"class":394},[363,2471,2472,2474,2477,2479,2482],{"class":365,"line":404},[363,2473,1034],{"class":432},[363,2475,2476],{"class":439}," toggle ",[363,2478,443],{"class":394},[363,2480,2481],{"class":473}," UISwitch",[363,2483,933],{"class":394},[363,2485,2486,2488,2491,2494,2496,2498,2501,2504,2507],{"class":365,"line":411},[363,2487,457],{"class":432},[363,2489,2490],{"class":439}," onChange: ",[363,2492,2493],{"class":394},"((",[363,2495,1748],{"class":373},[363,2497,489],{"class":394},[363,2499,2500],{"class":394}," ->",[363,2502,2503],{"class":373}," Void",[363,2505,2506],{"class":394},")?",[363,2508,2509],{"class":407}," \u002F\u002F 父视图填进来，用来接收新值\n",[363,2511,2512],{"class":365,"line":417},[363,2513,381],{"emptyLinePlaceholder":380},[363,2515,2516,2518,2521,2523,2527,2530,2532],{"class":365,"line":423},[363,2517,2077],{"class":387},[363,2519,2520],{"class":387}," init",[363,2522,477],{"class":394},[363,2524,2526],{"class":2525},"sjYur","frame",[363,2528,2529],{"class":439},": CGRect",[363,2531,489],{"class":394},[363,2533,496],{"class":394},[363,2535,2536,2538,2541,2543,2545,2547,2550],{"class":365,"line":449},[363,2537,2092],{"class":439},[363,2539,2540],{"class":387},"init",[363,2542,477],{"class":394},[363,2544,2526],{"class":473},[363,2546,395],{"class":394},[363,2548,2549],{"class":439}," frame",[363,2551,738],{"class":394},[363,2553,2554,2557,2559,2561,2563,2565,2567,2569,2571,2574,2576,2578,2580,2582,2585],{"class":365,"line":454},[363,2555,2556],{"class":439},"        toggle.",[363,2558,2274],{"class":473},[363,2560,477],{"class":394},[363,2562,2279],{"class":439},[363,2564,2282],{"class":473},[363,2566,395],{"class":394},[363,2568,2287],{"class":473},[363,2570,477],{"class":394},[363,2572,2573],{"class":439},"switched",[363,2575,489],{"class":394},[363,2577,1240],{"class":439},[363,2579,2260],{"class":473},[363,2581,395],{"class":394},[363,2583,2584],{"class":439}," .valueChanged",[363,2586,738],{"class":394},[363,2588,2589,2592,2594,2597],{"class":365,"line":470},[363,2590,2591],{"class":473},"        addSubview",[363,2593,477],{"class":394},[363,2595,2596],{"class":439},"toggle",[363,2598,738],{"class":394},[363,2600,2601],{"class":365,"line":499},[363,2602,523],{"class":394},[363,2604,2605],{"class":365,"line":514},[363,2606,381],{"emptyLinePlaceholder":380},[363,2608,2609,2612,2615,2617,2620,2623,2625],{"class":365,"line":520},[363,2610,2611],{"class":387},"    required",[363,2613,2614],{"class":387}," init?",[363,2616,477],{"class":394},[363,2618,2619],{"class":2525},"coder",[363,2621,2622],{"class":439},": NSCoder",[363,2624,489],{"class":394},[363,2626,496],{"class":394},[363,2628,2629,2632,2634,2636,2639,2641],{"class":365,"line":526},[363,2630,2631],{"class":473},"        fatalError",[363,2633,477],{"class":394},[363,2635,480],{"class":394},[363,2637,2638],{"class":1234},"init(coder:) has not been implemented",[363,2640,480],{"class":394},[363,2642,738],{"class":394},[363,2644,2645],{"class":365,"line":682},[363,2646,523],{"class":394},[363,2648,2649],{"class":365,"line":699},[363,2650,381],{"emptyLinePlaceholder":380},[363,2652,2653,2655,2657,2659,2661,2664,2666],{"class":365,"line":704},[363,2654,426],{"class":394},[363,2656,2342],{"class":387},[363,2658,433],{"class":432},[363,2660,2080],{"class":387},[363,2662,2663],{"class":473}," switched",[363,2665,866],{"class":394},[363,2667,496],{"class":394},[363,2669,2670,2673,2676,2679],{"class":365,"line":717},[363,2671,2672],{"class":439},"        onChange",[363,2674,2675],{"class":394},"?(",[363,2677,2678],{"class":439},"toggle.isOn",[363,2680,738],{"class":394},[363,2682,2683],{"class":365,"line":723},[363,2684,523],{"class":394},[363,2686,2687],{"class":365,"line":741},[363,2688,529],{"class":394},[215,2690,2693],{"className":356,"code":2691,"filename":2692,"language":359,"meta":221,"style":221},"import UIKit\n\nfinal class SettingsViewController: UIViewController {\n    private var notificationsOn = false\n    private let row = ToggleRow()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        row.onChange = { [weak self] isOn in\n            self?.notificationsOn = isOn \u002F\u002F 源仍在设置页\n            self?.apply()\n        }\n    }\n\n    private func apply() {\n        title = notificationsOn ? \"开\" : \"关\"\n    }\n}\n","SettingsViewController.swift",[198,2694,2695,2701,2705,2720,2732,2747,2751,2763,2771,2792,2811,2825,2829,2833,2837,2849,2877,2881],{"__ignoreMap":221},[363,2696,2697,2699],{"class":365,"line":366},[363,2698,370],{"class":369},[363,2700,2016],{"class":373},[363,2702,2703],{"class":365,"line":377},[363,2704,381],{"emptyLinePlaceholder":380},[363,2706,2707,2709,2711,2714,2716,2718],{"class":365,"line":384},[363,2708,819],{"class":387},[363,2710,822],{"class":387},[363,2712,2713],{"class":373}," SettingsViewController",[363,2715,395],{"class":394},[363,2717,2034],{"class":373},[363,2719,401],{"class":394},[363,2721,2722,2724,2726,2728,2730],{"class":365,"line":404},[363,2723,832],{"class":432},[363,2725,436],{"class":432},[363,2727,1853],{"class":439},[363,2729,443],{"class":394},[363,2731,1859],{"class":1858},[363,2733,2734,2736,2738,2741,2743,2745],{"class":365,"line":411},[363,2735,832],{"class":432},[363,2737,2058],{"class":432},[363,2739,2740],{"class":439}," row ",[363,2742,443],{"class":394},[363,2744,2462],{"class":473},[363,2746,933],{"class":394},[363,2748,2749],{"class":365,"line":417},[363,2750,381],{"emptyLinePlaceholder":380},[363,2752,2753,2755,2757,2759,2761],{"class":365,"line":423},[363,2754,2077],{"class":387},[363,2756,2080],{"class":387},[363,2758,2083],{"class":473},[363,2760,866],{"class":394},[363,2762,496],{"class":394},[363,2764,2765,2767,2769],{"class":365,"line":449},[363,2766,2092],{"class":439},[363,2768,2095],{"class":473},[363,2770,933],{"class":394},[363,2772,2773,2776,2778,2780,2783,2786,2789],{"class":365,"line":454},[363,2774,2775],{"class":439},"        row.onChange ",[363,2777,443],{"class":394},[363,2779,640],{"class":394},[363,2781,2782],{"class":439}," [",[363,2784,2785],{"class":432},"weak",[363,2787,2788],{"class":439}," self] isOn ",[363,2790,2791],{"class":369},"in\n",[363,2793,2794,2797,2800,2803,2805,2808],{"class":365,"line":470},[363,2795,2796],{"class":439},"            self",[363,2798,2799],{"class":394},"?",[363,2801,2802],{"class":439},".notificationsOn ",[363,2804,443],{"class":394},[363,2806,2807],{"class":439}," isOn ",[363,2809,2810],{"class":407},"\u002F\u002F 源仍在设置页\n",[363,2812,2813,2815,2817,2820,2823],{"class":365,"line":499},[363,2814,2796],{"class":439},[363,2816,2799],{"class":394},[363,2818,2819],{"class":439},".",[363,2821,2822],{"class":473},"apply",[363,2824,933],{"class":394},[363,2826,2827],{"class":365,"line":514},[363,2828,517],{"class":394},[363,2830,2831],{"class":365,"line":520},[363,2832,523],{"class":394},[363,2834,2835],{"class":365,"line":526},[363,2836,381],{"emptyLinePlaceholder":380},[363,2838,2839,2841,2843,2845,2847],{"class":365,"line":682},[363,2840,832],{"class":432},[363,2842,2080],{"class":387},[363,2844,2386],{"class":473},[363,2846,866],{"class":394},[363,2848,496],{"class":394},[363,2850,2851,2854,2856,2858,2860,2862,2865,2867,2870,2872,2875],{"class":365,"line":699},[363,2852,2853],{"class":439},"        title ",[363,2855,443],{"class":394},[363,2857,1853],{"class":439},[363,2859,2799],{"class":394},[363,2861,2400],{"class":394},[363,2863,2864],{"class":1234},"开",[363,2866,480],{"class":394},[363,2868,2869],{"class":394}," :",[363,2871,2400],{"class":394},[363,2873,2874],{"class":1234},"关",[363,2876,2409],{"class":394},[363,2878,2879],{"class":365,"line":704},[363,2880,523],{"class":394},[363,2882,2883],{"class":365,"line":717},[363,2884,529],{"class":394},[179,2886,2887,2888,2891,2892,2894,2895,2898],{},"闭包被 ",[198,2889,2890],{},"row"," 留着。闭包再强引用控制器，控制器又留着 ",[198,2893,2890],{},"，两边互相持有。",[198,2896,2897],{},"[weak self]"," 把控制器那边断开。一个开关用闭包即可。同一块视图要交好几件事，收成 delegate，子视图调用父视图的方法。",[179,2900,2901,2902,2905],{},"父视图把数据推进子视图，走配置方法：",[198,2903,2904],{},"cell.configure(item)","。Cell 显示这份值，源仍在控制器或它的数组里。",[327,2907,2908],{"id":2908},"列表快照",[179,2910,2911],{},"列表的状态是「有哪些行、按什么顺序」。改数组之后做一份快照交进去，由 diffable data source 处理插入和删除。",[215,2913,2916],{"className":356,"code":2914,"filename":2915,"language":359,"meta":221,"style":221},"import UIKit\n\nstruct Item: Identifiable {\n    let id: UUID\n    var title: String\n}\n\nenum Section: Hashable {\n    case main\n}\n\nfinal class ItemListViewController: UIViewController {\n    private var dataSource: UITableViewDiffableDataSource\u003CSection, Item.ID>!\n\n    func apply(items: [Item]) {\n        var snapshot = NSDiffableDataSourceSnapshot\u003CSection, Item.ID>()\n        snapshot.appendSections([.main])\n        snapshot.appendItems(items.map(\\.id)) \u002F\u002F 快照里只放稳定 id\n        dataSource.apply(snapshot, animatingDifferences: true)\n    }\n}\n","ItemListViewController.swift",[198,2917,2918,2924,2928,2942,2949,2959,2963,2967,2982,2990,2994,2998,3013,3031,3035,3060,3080,3095,3121,3143,3147],{"__ignoreMap":221},[363,2919,2920,2922],{"class":365,"line":366},[363,2921,370],{"class":369},[363,2923,2016],{"class":373},[363,2925,2926],{"class":365,"line":377},[363,2927,381],{"emptyLinePlaceholder":380},[363,2929,2930,2932,2935,2937,2940],{"class":365,"line":384},[363,2931,388],{"class":387},[363,2933,2934],{"class":373}," Item",[363,2936,395],{"class":394},[363,2938,2939],{"class":373}," Identifiable ",[363,2941,401],{"class":394},[363,2943,2944,2946],{"class":365,"line":404},[363,2945,1034],{"class":432},[363,2947,2948],{"class":439}," id: UUID\n",[363,2950,2951,2953,2956],{"class":365,"line":411},[363,2952,457],{"class":432},[363,2954,2955],{"class":439}," title: ",[363,2957,2958],{"class":373},"String\n",[363,2960,2961],{"class":365,"line":417},[363,2962,529],{"class":394},[363,2964,2965],{"class":365,"line":423},[363,2966,381],{"emptyLinePlaceholder":380},[363,2968,2969,2972,2975,2977,2980],{"class":365,"line":449},[363,2970,2971],{"class":387},"enum",[363,2973,2974],{"class":373}," Section",[363,2976,395],{"class":394},[363,2978,2979],{"class":373}," Hashable ",[363,2981,401],{"class":394},[363,2983,2984,2987],{"class":365,"line":454},[363,2985,2986],{"class":387},"    case",[363,2988,2989],{"class":439}," main\n",[363,2991,2992],{"class":365,"line":470},[363,2993,529],{"class":394},[363,2995,2996],{"class":365,"line":499},[363,2997,381],{"emptyLinePlaceholder":380},[363,2999,3000,3002,3004,3007,3009,3011],{"class":365,"line":514},[363,3001,819],{"class":387},[363,3003,822],{"class":387},[363,3005,3006],{"class":373}," ItemListViewController",[363,3008,395],{"class":394},[363,3010,2034],{"class":373},[363,3012,401],{"class":394},[363,3014,3015,3017,3019,3022,3025,3028],{"class":365,"line":520},[363,3016,832],{"class":432},[363,3018,436],{"class":432},[363,3020,3021],{"class":439}," dataSource: UITableViewDiffableDataSource",[363,3023,3024],{"class":394},"\u003C",[363,3026,3027],{"class":439},"Section, Item.ID",[363,3029,3030],{"class":394},">!\n",[363,3032,3033],{"class":365,"line":526},[363,3034,381],{"emptyLinePlaceholder":380},[363,3036,3037,3039,3041,3043,3046,3049,3052,3055,3058],{"class":365,"line":682},[363,3038,860],{"class":387},[363,3040,2386],{"class":473},[363,3042,477],{"class":394},[363,3044,3045],{"class":2525},"items",[363,3047,3048],{"class":439},": ",[363,3050,3051],{"class":394},"[",[363,3053,3054],{"class":439},"Item",[363,3056,3057],{"class":394},"])",[363,3059,496],{"class":394},[363,3061,3062,3065,3068,3070,3073,3075,3077],{"class":365,"line":699},[363,3063,3064],{"class":432},"        var",[363,3066,3067],{"class":439}," snapshot ",[363,3069,443],{"class":394},[363,3071,3072],{"class":439}," NSDiffableDataSourceSnapshot",[363,3074,3024],{"class":394},[363,3076,3027],{"class":439},[363,3078,3079],{"class":394},">()\n",[363,3081,3082,3085,3088,3090,3093],{"class":365,"line":704},[363,3083,3084],{"class":439},"        snapshot.",[363,3086,3087],{"class":473},"appendSections",[363,3089,477],{"class":394},[363,3091,3092],{"class":439},"[.main]",[363,3094,738],{"class":394},[363,3096,3097,3099,3102,3104,3107,3110,3112,3115,3118],{"class":365,"line":717},[363,3098,3084],{"class":439},[363,3100,3101],{"class":473},"appendItems",[363,3103,477],{"class":394},[363,3105,3106],{"class":439},"items.",[363,3108,3109],{"class":473},"map",[363,3111,477],{"class":394},[363,3113,3114],{"class":439},"\\.id",[363,3116,3117],{"class":394},"))",[363,3119,3120],{"class":407}," \u002F\u002F 快照里只放稳定 id\n",[363,3122,3123,3126,3128,3130,3133,3136,3138,3141],{"class":365,"line":723},[363,3124,3125],{"class":439},"        dataSource.",[363,3127,2822],{"class":473},[363,3129,477],{"class":394},[363,3131,3132],{"class":439},"snapshot, ",[363,3134,3135],{"class":473},"animatingDifferences",[363,3137,395],{"class":394},[363,3139,3140],{"class":1858}," true",[363,3142,738],{"class":394},[363,3144,3145],{"class":365,"line":741},[363,3146,523],{"class":394},[363,3148,3149],{"class":365,"line":746},[363,3150,529],{"class":394},[179,3152,3153,3155,3156,3159,3160,3163],{},[198,3154,3054],{}," 遵守 ",[198,3157,3158],{},"Identifiable","，",[198,3161,3162],{},"ID"," 稳定。Cell 用这个 id 去取标题。把会变的标题算进哈希，同一次编辑会被看成删掉旧行、再插入新行。",[327,3165,3167],{"id":3166},"广播kvo单例","广播、KVO、单例",[228,3169,3170,3183],{},[231,3171,3172],{},[234,3173,3174,3177,3180],{},[237,3175,3176],{},"通道",[237,3178,3179],{},"方向",[237,3181,3182],{},"我用它的时候",[247,3184,3185,3196,3207,3221,3234,3245],{},[234,3186,3187,3190,3193],{},[252,3188,3189],{},"target-action",[252,3191,3192],{},"控件 → 控制器",[252,3194,3195],{},"按钮、开关",[234,3197,3198,3201,3204],{},[252,3199,3200],{},"delegate、闭包",[252,3202,3203],{},"子 → 父",[252,3205,3206],{},"一行开关、cell 上的按钮",[234,3208,3209,3212,3215],{},[252,3210,3211],{},"配置方法",[252,3213,3214],{},"父 → 子",[252,3216,3217,3220],{},[198,3218,3219],{},"configure(_:)"," 把模型推进 cell",[234,3222,3223,3228,3231],{},[252,3224,3225],{},[198,3226,3227],{},"NotificationCenter",[252,3229,3230],{},"谁都能发，谁都能听",[252,3232,3233],{},"键盘、进后台",[234,3235,3236,3239,3242],{},[252,3237,3238],{},"KVO",[252,3240,3241],{},"观察某个对象的属性",[252,3243,3244],{},"系统对象上还在用",[234,3246,3247,3250,3253],{},[252,3248,3249],{},"单例",[252,3251,3252],{},"全局读写",[252,3254,3255],{},"当成外部存储，读进这一页的模型",[179,3257,3258,3260],{},[198,3259,3227],{}," 适合「发生了一件事」。某一页的字段若靠通知传来传去，发送方和接收方要对着字符串才能对上。",[179,3262,3263,3266,3267,335],{},[198,3264,3265],{},"Session.shared"," 这种单例谁都能写。页面从它读进自己的模型，改完再写回去。刷新仍走这一页的 ",[198,3268,1995],{},[179,3270,3271,3272,3275,3276,3279],{},"控制器因此会变大：它同时是模型的主人、网络的回调、控件的操作者，还顺手 ",[198,3273,3274],{},"push"," 下一页。跳转单独记在 ",[3277,3278,126],"a",{"href":127},"。剩下的，是下一节要搬出去的部分。",[207,3281,3282],{"id":3282},"进阶架构",[179,3284,3285],{},"这些模式回答的是同一件事：状态只有一条改法，界面只负责把当前值画出来。",[228,3287,3288,3304],{},[231,3289,3290],{},[234,3291,3292,3295,3298,3301],{},[237,3293,3294],{},"模式",[237,3296,3297],{},"状态放哪",[237,3299,3300],{},"界面怎么跟上",[237,3302,3303],{},"我什么时候用",[247,3305,3306,3322,3336,3350,3364],{},[234,3307,3308,3311,3314,3319],{},[252,3309,3310],{},"MVC",[252,3312,3313],{},"控制器",[252,3315,3316,3317],{},"控制器调用 ",[198,3318,2822],{},[252,3320,3321],{},"一页、几个字段",[234,3323,3324,3327,3330,3333],{},[252,3325,3326],{},"MVP",[252,3328,3329],{},"Presenter",[252,3331,3332],{},"Presenter 调 View 协议上的方法",[252,3334,3335],{},"逻辑要单测，视图保持只会显示",[234,3337,3338,3341,3344,3347],{},[252,3339,3340],{},"MVVM",[252,3342,3343],{},"ViewModel",[252,3345,3346],{},"UIKit 订阅；SwiftUI 直接读",[252,3348,3349],{},"这一屏的状态和动作要离开控制器",[234,3351,3352,3355,3358,3361],{},[252,3353,3354],{},"VIPER",[252,3356,3357],{},"Interactor 持有实体，Presenter 准备要显示的字段",[252,3359,3360],{},"Presenter 调 View",[252,3362,3363],{},"模块边界要写死、人多",[234,3365,3366,3369,3374,3379],{},[252,3367,3368],{},"单向数据流",[252,3370,3371,3372],{},"一份值类型 ",[198,3373,429],{},[252,3375,3376,3377],{},"渲染这份 ",[198,3378,429],{},[252,3380,3381],{},"状态转移要能复现，副作用要单独测",[327,3383,3326],{"id":3384},"mvp",[179,3386,3387,3388,3391,3392,3394],{},"View 是协议，只暴露 ",[198,3389,3390],{},"show(count:)"," 这种方法。Presenter 持有数字，用 ",[198,3393,2785],{}," 指回 View。按钮把事件交给 Presenter，Presenter 改完数，再叫 View。",[215,3396,3399],{"className":356,"code":3397,"filename":3398,"language":359,"meta":221,"style":221},"protocol CounterView: AnyObject {\n    func show(count: Int)\n}\n\nfinal class CounterPresenter {\n    private weak var view: CounterView? \u002F\u002F 控制器持有 Presenter，这里用 weak 避免成环\n    private var count = 0\n\n    init(view: CounterView) {\n        self.view = view\n    }\n\n    func increment() {\n        count += 1\n        view?.show(count: count)\n    }\n}\n","CounterPresenter.swift",[198,3400,3401,3415,3432,3436,3440,3451,3468,3480,3484,3501,3511,3515,3519,3529,3537,3560,3564],{"__ignoreMap":221},[363,3402,3403,3406,3408,3410,3413],{"class":365,"line":366},[363,3404,3405],{"class":387},"protocol",[363,3407,391],{"class":373},[363,3409,395],{"class":394},[363,3411,3412],{"class":373}," AnyObject ",[363,3414,401],{"class":394},[363,3416,3417,3419,3422,3424,3426,3428,3430],{"class":365,"line":377},[363,3418,860],{"class":387},[363,3420,3421],{"class":473}," show",[363,3423,477],{"class":394},[363,3425,486],{"class":2525},[363,3427,3048],{"class":439},[363,3429,1752],{"class":373},[363,3431,738],{"class":394},[363,3433,3434],{"class":365,"line":384},[363,3435,529],{"class":394},[363,3437,3438],{"class":365,"line":404},[363,3439,381],{"emptyLinePlaceholder":380},[363,3441,3442,3444,3446,3449],{"class":365,"line":411},[363,3443,819],{"class":387},[363,3445,822],{"class":387},[363,3447,3448],{"class":373}," CounterPresenter",[363,3450,496],{"class":394},[363,3452,3453,3455,3458,3460,3463,3465],{"class":365,"line":417},[363,3454,832],{"class":432},[363,3456,3457],{"class":432}," weak",[363,3459,436],{"class":432},[363,3461,3462],{"class":439}," view: CounterView",[363,3464,2799],{"class":394},[363,3466,3467],{"class":407}," \u002F\u002F 控制器持有 Presenter，这里用 weak 避免成环\n",[363,3469,3470,3472,3474,3476,3478],{"class":365,"line":423},[363,3471,832],{"class":432},[363,3473,436],{"class":432},[363,3475,440],{"class":439},[363,3477,443],{"class":394},[363,3479,446],{"class":432},[363,3481,3482],{"class":365,"line":449},[363,3483,381],{"emptyLinePlaceholder":380},[363,3485,3486,3489,3491,3494,3497,3499],{"class":365,"line":454},[363,3487,3488],{"class":387},"    init",[363,3490,477],{"class":394},[363,3492,3493],{"class":2525},"view",[363,3495,3496],{"class":439},": CounterView",[363,3498,489],{"class":394},[363,3500,496],{"class":394},[363,3502,3503,3506,3508],{"class":365,"line":470},[363,3504,3505],{"class":439},"        self.view ",[363,3507,443],{"class":394},[363,3509,3510],{"class":439}," view\n",[363,3512,3513],{"class":365,"line":499},[363,3514,523],{"class":394},[363,3516,3517],{"class":365,"line":514},[363,3518,381],{"emptyLinePlaceholder":380},[363,3520,3521,3523,3525,3527],{"class":365,"line":520},[363,3522,860],{"class":387},[363,3524,863],{"class":473},[363,3526,866],{"class":394},[363,3528,496],{"class":394},[363,3530,3531,3533,3535],{"class":365,"line":526},[363,3532,873],{"class":439},[363,3534,505],{"class":394},[363,3536,878],{"class":432},[363,3538,3539,3542,3544,3546,3549,3551,3553,3555,3558],{"class":365,"line":682},[363,3540,3541],{"class":439},"        view",[363,3543,2799],{"class":394},[363,3545,2819],{"class":439},[363,3547,3548],{"class":473},"show",[363,3550,477],{"class":394},[363,3552,486],{"class":473},[363,3554,395],{"class":394},[363,3556,3557],{"class":439}," count",[363,3559,738],{"class":394},[363,3561,3562],{"class":365,"line":699},[363,3563,523],{"class":394},[363,3565,3566],{"class":365,"line":704},[363,3567,529],{"class":394},[179,3569,3570,3571,3574,3575,3577,3578,335],{},"控制器遵守 ",[198,3572,3573],{},"CounterView","，在 ",[198,3576,3548],{}," 里改标签。控制器持有 Presenter。Presenter 不引用 UIKit 时，测试里换一个假的 View，就能断言它收到了 ",[198,3579,3580],{},"1",[179,3582,3583],{},"每个字段一条协议方法，字段一多协议就长。SwiftUI 直接读状态，MVP 主要留在 UIKit。",[327,3585,3340],{"id":3586},"mvvm",[179,3588,3589,3590,3593,3594,3596],{},"ViewModel 持有这一屏的状态和动作。视图把点击送进去，把状态画出来。ViewModel 不拿 ",[198,3591,3592],{},"UILabel","，也不 ",[198,3595,3274],{}," 下一页。",[179,3598,3599,3600,3602,3603,3605,3606,3608,3609,3612],{},"SwiftUI 里它就是上一节的 ",[198,3601,200],{}," 类，创建处用 ",[198,3604,256],{},"。UIKit 要把变化送到控件，用下一节的 Combine：",[198,3607,308],{}," 加 ",[198,3610,3611],{},"sink","。字段只有一个、一次赋值就刷新一次，用闭包就够。",[179,3614,3615,3616,3618],{},"两个界面看同一份时，共享的是这一个对象。UIKit 在外面创建一次，两个控制器的 ",[198,3617,2540],{}," 都接它。SwiftUI 在根上创建一次，用环境传下去。",[327,3620,3368],{"id":3368},[179,3622,3623,3624,3627],{},"ViewModel 的方法里可以同时加一、发请求、改 ",[198,3625,3626],{},"isLoading","。调用一深，这一帧的值是谁改的，要对着方法体找。",[179,3629,3630],{},"单向数据流把「现在的值」收成结构体，把「发生了什么」收成枚举。一个函数吃进旧状态和动作，写出新状态。视图只发动作，只读状态。",[215,3632,3635],{"className":356,"code":3633,"filename":3634,"language":359,"meta":221,"style":221},"struct CounterState: Equatable {\n    var count = 0\n    var fact: String?\n}\n\nenum CounterAction {\n    case incrementTapped\n    case factLoaded(String)\n}\n\nfunc reduce(_ state: inout CounterState, _ action: CounterAction) {\n    switch action {\n    case .incrementTapped:\n        state.count += 1 \u002F\u002F 同步改状态，不在这里发请求\n    case let .factLoaded(text):\n        state.fact = text \u002F\u002F 请求回来之后，用下一个动作写入\n    }\n}\n","CounterReduce.swift",[198,3636,3637,3651,3661,3673,3677,3681,3690,3697,3710,3714,3718,3755,3765,3775,3787,3806,3819,3823],{"__ignoreMap":221},[363,3638,3639,3641,3644,3646,3649],{"class":365,"line":366},[363,3640,388],{"class":387},[363,3642,3643],{"class":373}," CounterState",[363,3645,395],{"class":394},[363,3647,3648],{"class":373}," Equatable ",[363,3650,401],{"class":394},[363,3652,3653,3655,3657,3659],{"class":365,"line":377},[363,3654,457],{"class":432},[363,3656,440],{"class":439},[363,3658,443],{"class":394},[363,3660,446],{"class":432},[363,3662,3663,3665,3668,3670],{"class":365,"line":384},[363,3664,457],{"class":432},[363,3666,3667],{"class":439}," fact: ",[363,3669,1758],{"class":373},[363,3671,3672],{"class":394},"?\n",[363,3674,3675],{"class":365,"line":404},[363,3676,529],{"class":394},[363,3678,3679],{"class":365,"line":411},[363,3680,381],{"emptyLinePlaceholder":380},[363,3682,3683,3685,3688],{"class":365,"line":417},[363,3684,2971],{"class":387},[363,3686,3687],{"class":373}," CounterAction",[363,3689,496],{"class":394},[363,3691,3692,3694],{"class":365,"line":423},[363,3693,2986],{"class":387},[363,3695,3696],{"class":439}," incrementTapped\n",[363,3698,3699,3701,3704,3706,3708],{"class":365,"line":449},[363,3700,2986],{"class":387},[363,3702,3703],{"class":439}," factLoaded",[363,3705,477],{"class":394},[363,3707,1758],{"class":373},[363,3709,738],{"class":394},[363,3711,3712],{"class":365,"line":454},[363,3713,529],{"class":394},[363,3715,3716],{"class":365,"line":470},[363,3717,381],{"emptyLinePlaceholder":380},[363,3719,3720,3723,3726,3728,3731,3735,3737,3740,3743,3745,3748,3751,3753],{"class":365,"line":499},[363,3721,3722],{"class":387},"func",[363,3724,3725],{"class":473}," reduce",[363,3727,477],{"class":394},[363,3729,3730],{"class":473},"_",[363,3732,3734],{"class":3733},"sHdIc"," state",[363,3736,3048],{"class":439},[363,3738,3739],{"class":387},"inout",[363,3741,3742],{"class":439}," CounterState, ",[363,3744,3730],{"class":473},[363,3746,3747],{"class":3733}," action",[363,3749,3750],{"class":439},": CounterAction",[363,3752,489],{"class":394},[363,3754,496],{"class":394},[363,3756,3757,3760,3763],{"class":365,"line":514},[363,3758,3759],{"class":369},"    switch",[363,3761,3762],{"class":439}," action ",[363,3764,401],{"class":394},[363,3766,3767,3769,3772],{"class":365,"line":520},[363,3768,2986],{"class":369},[363,3770,3771],{"class":439}," .incrementTapped",[363,3773,3774],{"class":394},":\n",[363,3776,3777,3780,3782,3784],{"class":365,"line":526},[363,3778,3779],{"class":439},"        state.count ",[363,3781,505],{"class":394},[363,3783,508],{"class":432},[363,3785,3786],{"class":407}," \u002F\u002F 同步改状态，不在这里发请求\n",[363,3788,3789,3791,3793,3796,3799,3801,3803],{"class":365,"line":682},[363,3790,2986],{"class":369},[363,3792,2058],{"class":432},[363,3794,3795],{"class":439}," .",[363,3797,3798],{"class":473},"factLoaded",[363,3800,477],{"class":394},[363,3802,220],{"class":439},[363,3804,3805],{"class":394},"):\n",[363,3807,3808,3811,3813,3816],{"class":365,"line":699},[363,3809,3810],{"class":439},"        state.fact ",[363,3812,443],{"class":394},[363,3814,3815],{"class":439}," text ",[363,3817,3818],{"class":407},"\u002F\u002F 请求回来之后，用下一个动作写入\n",[363,3820,3821],{"class":365,"line":704},[363,3822,523],{"class":394},[363,3824,3825],{"class":365,"line":717},[363,3826,529],{"class":394},[179,3828,3829,3830,3833,3834,335,3837,3840],{},"请求留在这个函数外面。点按钮若还要拉一段说明，调用方先发出 ",[198,3831,3832],{},".incrementTapped","，请求回来再发出 ",[198,3835,3836],{},".factLoaded",[198,3838,3839],{},"reduce"," 保持同步。测试就是：给定这个状态和这个动作，得到那个状态。",[179,3842,3843,3844,3846,3847,3849,3850,335],{},"界面侧仍是 UIKit 的 ",[198,3845,2822],{},"，或 SwiftUI 的 ",[198,3848,320],{},"。手上永远是一份 ",[198,3851,3852],{},"CounterState",[179,3854,3855,3856,3858,3859,3862],{},"这和 MVVM 可以叠在一起：ViewModel 内部用 ",[198,3857,3839],{}," 改字段，对外仍是 ",[198,3860,3861],{},"increment()","。小页面这样就够。动作和副作用要在模块之间传递时，再做成下一节的库。",[327,3864,3354],{"id":3865},"viper",[179,3867,3868],{},"VIPER 把一屏拆成五块。",[228,3870,3871,3881],{},[231,3872,3873],{},[234,3874,3875,3878],{},[237,3876,3877],{},"角色",[237,3879,3880],{},"做什么",[247,3882,3883,3890,3898,3905,3913],{},[234,3884,3885,3887],{},[252,3886,316],{},[252,3888,3889],{},"显示，把交互交出去",[234,3891,3892,3895],{},[252,3893,3894],{},"Interactor",[252,3896,3897],{},"用例：网络、存储",[234,3899,3900,3902],{},[252,3901,3329],{},[252,3903,3904],{},"把实体变成界面要的字段",[234,3906,3907,3910],{},[252,3908,3909],{},"Entity",[252,3911,3912],{},"数据",[234,3914,3915,3918],{},[252,3916,3917],{},"Router",[252,3919,3920],{},"离开这一屏",[179,3922,3923],{},"每一块一个协议，模块可以单独换。一个设置页也上五套类型，文件数会先涨上来。我在这块业务跨多屏、而且要脱离界面测试时，才拆到这个粒度。",[179,3925,3926,3927,3929],{},"再粗一点的分法是三层：界面、用例、数据源。用例返回这一屏要的模型，界面只 ",[198,3928,2822],{},"。单向数据流可以叠在上面：Interactor 跑副作用，Reducer 改状态，Router 管跳转。",[179,3931,3932,3933,3936,3937,3939],{},"流程状态和页面状态分开。栈里有哪些页，用路由那篇的 ",[198,3934,3935],{},"path"," 或导航控制器。这一页上的字段留在模型或 ",[198,3938,429],{}," 里。",[207,3941,3942],{"id":3942},"主流状态管理",[179,3944,3945,3946,3948],{},"视图最后仍是读状态、发动作。梯子上的 ",[198,3947,256],{}," 管控件自己的值。下面这些负责把变化送出去，或规定这一屏的状态怎么变。三方库再把动作和副作用收成同一套形状，方便换掉依赖来测试。",[228,3950,3951,3966],{},[231,3952,3953],{},[234,3954,3955,3957,3960,3963],{},[237,3956,1235],{},[237,3958,3959],{},"它规定什么",[237,3961,3962],{},"常见搭配",[237,3964,3965],{},"我怎么看",[247,3967,3968,3985,4003,4020,4034],{},[234,3969,3970,3973,3976,3982],{},[252,3971,3972],{},"Combine",[252,3974,3975],{},"值一变就送出去",[252,3977,3978,3979,3981],{},"UIKit 的 ",[198,3980,3611],{},"；连续输入再加工",[252,3983,3984],{},"系统框架。状态仍放在模型里",[234,3986,3987,3990,3993,3996],{},[252,3988,3989],{},"RxSwift",[252,3991,3992],{},"一条可取消的事件流",[252,3994,3995],{},"UIKit + MVVM",[252,3997,3998,3999,4002],{},"老工程的绑定层。新页面的请求我用 ",[198,4000,4001],{},"async"," 方法",[234,4004,4005,4008,4011,4014],{},[252,4006,4007],{},"ReactorKit",[252,4009,4010],{},"Action → Mutation → State",[252,4012,4013],{},"必须有 RxSwift",[252,4015,4016,4017],{},"每屏一个 Reactor，副作用在 ",[198,4018,4019],{},"mutate",[234,4021,4022,4025,4028,4031],{},[252,4023,4024],{},"ReSwift",[252,4026,4027],{},"一个全局 Store",[252,4029,4030],{},"UIKit",[252,4032,4033],{},"Redux 的 Swift 版。一个店装下整个 App",[234,4035,4036,4039,4042,4045],{},[252,4037,4038],{},"TCA",[252,4040,4041],{},"Reducer、Effect、Store，功能可以嵌套",[252,4043,4044],{},"SwiftUI，也能接 UIKit",[252,4046,4047],{},"现在讨论最多的一套。概念多，测试是它的主场",[327,4049,3972],{"id":4050},"combine",[179,4052,4053,4055,4056,4058,4059,4061],{},[198,4054,308],{}," 包住一个属性。",[198,4057,542],{}," 是发布者，每次赋值都发出新值。UIKit 用 ",[198,4060,3611],{}," 接住，在里面写控件。",[215,4063,4066],{"className":356,"code":4064,"filename":4065,"language":359,"meta":221,"style":221},"import Combine\n\nfinal class CounterViewModel {\n    @Published private(set) var count = 0 \u002F\u002F $count 会在每次赋值时发出新值\n\n    func increment() {\n        count += 1\n    }\n}\n","CounterViewModel.swift",[198,4067,4068,4074,4078,4089,4114,4118,4128,4136,4140],{"__ignoreMap":221},[363,4069,4070,4072],{"class":365,"line":366},[363,4071,370],{"class":369},[363,4073,1533],{"class":373},[363,4075,4076],{"class":365,"line":377},[363,4077,381],{"emptyLinePlaceholder":380},[363,4079,4080,4082,4084,4087],{"class":365,"line":384},[363,4081,819],{"class":387},[363,4083,822],{"class":387},[363,4085,4086],{"class":373}," CounterViewModel",[363,4088,496],{"class":394},[363,4090,4091,4093,4095,4097,4099,4101,4103,4105,4107,4109,4111],{"class":365,"line":404},[363,4092,426],{"class":394},[363,4094,1571],{"class":387},[363,4096,433],{"class":432},[363,4098,477],{"class":394},[363,4100,837],{"class":387},[363,4102,489],{"class":394},[363,4104,436],{"class":432},[363,4106,440],{"class":439},[363,4108,443],{"class":394},[363,4110,848],{"class":432},[363,4112,4113],{"class":407}," \u002F\u002F $count 会在每次赋值时发出新值\n",[363,4115,4116],{"class":365,"line":411},[363,4117,381],{"emptyLinePlaceholder":380},[363,4119,4120,4122,4124,4126],{"class":365,"line":417},[363,4121,860],{"class":387},[363,4123,863],{"class":473},[363,4125,866],{"class":394},[363,4127,496],{"class":394},[363,4129,4130,4132,4134],{"class":365,"line":423},[363,4131,873],{"class":439},[363,4133,505],{"class":394},[363,4135,878],{"class":432},[363,4137,4138],{"class":365,"line":449},[363,4139,523],{"class":394},[363,4141,4142],{"class":365,"line":454},[363,4143,529],{"class":394},[215,4145,4148],{"className":356,"code":4146,"filename":4147,"language":359,"meta":221,"style":221},"import Combine\nimport UIKit\n\nfinal class CounterCombineViewController: UIViewController {\n    private let viewModel: CounterViewModel\n    private let label = UILabel()\n    private var cancellables = Set\u003CAnyCancellable>() \u002F\u002F 订阅留在这里，控制器在才继续收值\n\n    init(viewModel: CounterViewModel) {\n        self.viewModel = viewModel\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        viewModel.$count\n            .receive(on: DispatchQueue.main) \u002F\u002F 改控件排到主队列\n            .sink { [weak self] count in\n                self?.label.text = \"\\(count)\"\n            }\n            .store(in: &cancellables)\n    }\n\n    @objc private func increment() {\n        viewModel.increment()\n    }\n}\n","CounterCombineViewController.swift",[198,4149,4150,4156,4162,4166,4181,4190,4204,4229,4233,4249,4259,4285,4289,4293,4309,4323,4327,4331,4343,4351,4356,4378,4395,4417,4422,4444,4448,4452,4468,4477,4481],{"__ignoreMap":221},[363,4151,4152,4154],{"class":365,"line":366},[363,4153,370],{"class":369},[363,4155,1533],{"class":373},[363,4157,4158,4160],{"class":365,"line":377},[363,4159,370],{"class":369},[363,4161,2016],{"class":373},[363,4163,4164],{"class":365,"line":384},[363,4165,381],{"emptyLinePlaceholder":380},[363,4167,4168,4170,4172,4175,4177,4179],{"class":365,"line":404},[363,4169,819],{"class":387},[363,4171,822],{"class":387},[363,4173,4174],{"class":373}," CounterCombineViewController",[363,4176,395],{"class":394},[363,4178,2034],{"class":373},[363,4180,401],{"class":394},[363,4182,4183,4185,4187],{"class":365,"line":411},[363,4184,832],{"class":432},[363,4186,2058],{"class":432},[363,4188,4189],{"class":439}," viewModel: CounterViewModel\n",[363,4191,4192,4194,4196,4198,4200,4202],{"class":365,"line":417},[363,4193,832],{"class":432},[363,4195,2058],{"class":432},[363,4197,2061],{"class":439},[363,4199,443],{"class":394},[363,4201,2066],{"class":473},[363,4203,933],{"class":394},[363,4205,4206,4208,4210,4213,4215,4218,4220,4223,4226],{"class":365,"line":423},[363,4207,832],{"class":432},[363,4209,436],{"class":432},[363,4211,4212],{"class":439}," cancellables ",[363,4214,443],{"class":394},[363,4216,4217],{"class":373}," Set",[363,4219,3024],{"class":394},[363,4221,4222],{"class":439},"AnyCancellable",[363,4224,4225],{"class":394},">()",[363,4227,4228],{"class":407}," \u002F\u002F 订阅留在这里，控制器在才继续收值\n",[363,4230,4231],{"class":365,"line":449},[363,4232,381],{"emptyLinePlaceholder":380},[363,4234,4235,4237,4239,4242,4245,4247],{"class":365,"line":454},[363,4236,3488],{"class":387},[363,4238,477],{"class":394},[363,4240,4241],{"class":2525},"viewModel",[363,4243,4244],{"class":439},": CounterViewModel",[363,4246,489],{"class":394},[363,4248,496],{"class":394},[363,4250,4251,4254,4256],{"class":365,"line":470},[363,4252,4253],{"class":439},"        self.viewModel ",[363,4255,443],{"class":394},[363,4257,4258],{"class":439}," viewModel\n",[363,4260,4261,4263,4265,4267,4270,4272,4275,4277,4280,4282],{"class":365,"line":499},[363,4262,2092],{"class":439},[363,4264,2540],{"class":387},[363,4266,477],{"class":394},[363,4268,4269],{"class":473},"nibName",[363,4271,395],{"class":394},[363,4273,4274],{"class":394}," nil",[363,4276,1240],{"class":439},[363,4278,4279],{"class":473},"bundle",[363,4281,395],{"class":394},[363,4283,4284],{"class":394}," nil)\n",[363,4286,4287],{"class":365,"line":514},[363,4288,523],{"class":394},[363,4290,4291],{"class":365,"line":520},[363,4292,381],{"emptyLinePlaceholder":380},[363,4294,4295,4297,4299,4301,4303,4305,4307],{"class":365,"line":526},[363,4296,2611],{"class":387},[363,4298,2614],{"class":387},[363,4300,477],{"class":394},[363,4302,2619],{"class":2525},[363,4304,2622],{"class":439},[363,4306,489],{"class":394},[363,4308,496],{"class":394},[363,4310,4311,4313,4315,4317,4319,4321],{"class":365,"line":682},[363,4312,2631],{"class":473},[363,4314,477],{"class":394},[363,4316,480],{"class":394},[363,4318,2638],{"class":1234},[363,4320,480],{"class":394},[363,4322,738],{"class":394},[363,4324,4325],{"class":365,"line":699},[363,4326,523],{"class":394},[363,4328,4329],{"class":365,"line":704},[363,4330,381],{"emptyLinePlaceholder":380},[363,4332,4333,4335,4337,4339,4341],{"class":365,"line":717},[363,4334,2077],{"class":387},[363,4336,2080],{"class":387},[363,4338,2083],{"class":473},[363,4340,866],{"class":394},[363,4342,496],{"class":394},[363,4344,4345,4347,4349],{"class":365,"line":723},[363,4346,2092],{"class":439},[363,4348,2095],{"class":473},[363,4350,933],{"class":394},[363,4352,4353],{"class":365,"line":741},[363,4354,4355],{"class":439},"        viewModel.$count\n",[363,4357,4358,4360,4363,4365,4368,4370,4373,4375],{"class":365,"line":746},[363,4359,1383],{"class":439},[363,4361,4362],{"class":473},"receive",[363,4364,477],{"class":394},[363,4366,4367],{"class":473},"on",[363,4369,395],{"class":394},[363,4371,4372],{"class":439}," DispatchQueue.main",[363,4374,489],{"class":394},[363,4376,4377],{"class":407}," \u002F\u002F 改控件排到主队列\n",[363,4379,4380,4382,4384,4386,4388,4390,4393],{"class":365,"line":990},[363,4381,1383],{"class":439},[363,4383,3611],{"class":473},[363,4385,640],{"class":394},[363,4387,2782],{"class":439},[363,4389,2785],{"class":432},[363,4391,4392],{"class":439}," self] count ",[363,4394,2791],{"class":369},[363,4396,4397,4400,4402,4405,4407,4409,4411,4413,4415],{"class":365,"line":995},[363,4398,4399],{"class":439},"                self",[363,4401,2799],{"class":394},[363,4403,4404],{"class":439},".label.text ",[363,4406,443],{"class":394},[363,4408,2400],{"class":394},[363,4410,483],{"class":394},[363,4412,486],{"class":439},[363,4414,489],{"class":394},[363,4416,2409],{"class":394},[363,4418,4419],{"class":365,"line":1000},[363,4420,4421],{"class":394},"            }\n",[363,4423,4424,4426,4429,4431,4434,4436,4439,4442],{"class":365,"line":1005},[363,4425,1383],{"class":439},[363,4427,4428],{"class":473},"store",[363,4430,477],{"class":394},[363,4432,4433],{"class":473},"in",[363,4435,395],{"class":394},[363,4437,4438],{"class":394}," &",[363,4440,4441],{"class":439},"cancellables",[363,4443,738],{"class":394},[363,4445,4446],{"class":365,"line":1019},[363,4447,523],{"class":394},[363,4449,4450],{"class":365,"line":1025},[363,4451,381],{"emptyLinePlaceholder":380},[363,4453,4454,4456,4458,4460,4462,4464,4466],{"class":365,"line":1031},[363,4455,426],{"class":394},[363,4457,2342],{"class":387},[363,4459,433],{"class":432},[363,4461,2080],{"class":387},[363,4463,863],{"class":473},[363,4465,866],{"class":394},[363,4467,496],{"class":394},[363,4469,4470,4473,4475],{"class":365,"line":1040},[363,4471,4472],{"class":439},"        viewModel.",[363,4474,978],{"class":473},[363,4476,933],{"class":394},[363,4478,4479],{"class":365,"line":1045},[363,4480,523],{"class":394},[363,4482,4483],{"class":365,"line":1058},[363,4484,529],{"class":394},[179,4486,4487],{},"这里有三件容易漏的事。",[1917,4489,4490,4498,4504],{},[1920,4491,4492,4494,4495,4497],{},[198,4493,3611],{}," 返回的 ",[198,4496,4222],{}," 放进控制器的集合。用局部变量接，方法一结束，订阅就取消了。",[1920,4499,4500,4501,4503],{},"闭包用 ",[198,4502,2897],{},"。集合留着订阅，订阅再强引用控制器，就成环了。",[1920,4505,4506,4509,4510,4512],{},[198,4507,4508],{},".receive(on: DispatchQueue.main)"," 把改控件排到主队列。它是异步的：当时已经在主线程，标签也会等到这一轮事件之后再改。上游线程确定是主线程时，可以直接在 ",[198,4511,3611],{}," 里写标签。",[179,4514,4515,4516,4518,4519,335],{},"按钮只调用 ",[198,4517,3861],{},"。标签只在订阅里写。视图不保存第二份 ",[198,4520,486],{},[179,4522,4523,4524,4526],{},"一次赋值对应一次刷新，上面的 ",[198,4525,3611],{}," 就够。字是连续打进来的，再让流停一停、去掉重复。",[215,4528,4531],{"className":356,"code":4529,"filename":4530,"language":359,"meta":221,"style":221},"import Combine\n\nfinal class SearchViewModel {\n    @Published var keyword = \"\"\n    private var cancellables = Set\u003CAnyCancellable>()\n\n    init() {\n        $keyword\n            .debounce(for: .milliseconds(300), scheduler: RunLoop.main) \u002F\u002F 停手 300 毫秒再往下\n            .removeDuplicates() \u002F\u002F 同一个词不重复搜索\n            .sink { [weak self] text in\n                self?.search(text)\n            }\n            .store(in: &cancellables)\n    }\n\n    private func search(_ text: String) {}\n}\n","SearchKeyword.swift",[198,4532,4533,4539,4543,4554,4569,4587,4591,4599,4604,4644,4656,4673,4690,4694,4712,4716,4720,4745],{"__ignoreMap":221},[363,4534,4535,4537],{"class":365,"line":366},[363,4536,370],{"class":369},[363,4538,1533],{"class":373},[363,4540,4541],{"class":365,"line":377},[363,4542,381],{"emptyLinePlaceholder":380},[363,4544,4545,4547,4549,4552],{"class":365,"line":384},[363,4546,819],{"class":387},[363,4548,822],{"class":387},[363,4550,4551],{"class":373}," SearchViewModel",[363,4553,496],{"class":394},[363,4555,4556,4558,4560,4562,4565,4567],{"class":365,"line":404},[363,4557,426],{"class":394},[363,4559,1571],{"class":387},[363,4561,436],{"class":432},[363,4563,4564],{"class":439}," keyword ",[363,4566,443],{"class":394},[363,4568,1163],{"class":394},[363,4570,4571,4573,4575,4577,4579,4581,4583,4585],{"class":365,"line":411},[363,4572,832],{"class":432},[363,4574,436],{"class":432},[363,4576,4212],{"class":439},[363,4578,443],{"class":394},[363,4580,4217],{"class":373},[363,4582,3024],{"class":394},[363,4584,4222],{"class":439},[363,4586,3079],{"class":394},[363,4588,4589],{"class":365,"line":417},[363,4590,381],{"emptyLinePlaceholder":380},[363,4592,4593,4595,4597],{"class":365,"line":423},[363,4594,3488],{"class":387},[363,4596,866],{"class":394},[363,4598,496],{"class":394},[363,4600,4601],{"class":365,"line":449},[363,4602,4603],{"class":439},"        $keyword\n",[363,4605,4606,4608,4611,4613,4615,4617,4619,4622,4624,4627,4629,4631,4634,4636,4639,4641],{"class":365,"line":454},[363,4607,1383],{"class":439},[363,4609,4610],{"class":473},"debounce",[363,4612,477],{"class":394},[363,4614,2260],{"class":473},[363,4616,395],{"class":394},[363,4618,3795],{"class":439},[363,4620,4621],{"class":473},"milliseconds",[363,4623,477],{"class":394},[363,4625,4626],{"class":432},"300",[363,4628,489],{"class":394},[363,4630,1240],{"class":439},[363,4632,4633],{"class":473},"scheduler",[363,4635,395],{"class":394},[363,4637,4638],{"class":439}," RunLoop.main",[363,4640,489],{"class":394},[363,4642,4643],{"class":407}," \u002F\u002F 停手 300 毫秒再往下\n",[363,4645,4646,4648,4651,4653],{"class":365,"line":470},[363,4647,1383],{"class":439},[363,4649,4650],{"class":473},"removeDuplicates",[363,4652,866],{"class":394},[363,4654,4655],{"class":407}," \u002F\u002F 同一个词不重复搜索\n",[363,4657,4658,4660,4662,4664,4666,4668,4671],{"class":365,"line":499},[363,4659,1383],{"class":439},[363,4661,3611],{"class":473},[363,4663,640],{"class":394},[363,4665,2782],{"class":439},[363,4667,2785],{"class":432},[363,4669,4670],{"class":439}," self] text ",[363,4672,2791],{"class":369},[363,4674,4675,4677,4679,4681,4684,4686,4688],{"class":365,"line":514},[363,4676,4399],{"class":439},[363,4678,2799],{"class":394},[363,4680,2819],{"class":439},[363,4682,4683],{"class":473},"search",[363,4685,477],{"class":394},[363,4687,220],{"class":439},[363,4689,738],{"class":394},[363,4691,4692],{"class":365,"line":520},[363,4693,4421],{"class":394},[363,4695,4696,4698,4700,4702,4704,4706,4708,4710],{"class":365,"line":526},[363,4697,1383],{"class":439},[363,4699,4428],{"class":473},[363,4701,477],{"class":394},[363,4703,4433],{"class":473},[363,4705,395],{"class":394},[363,4707,4438],{"class":394},[363,4709,4441],{"class":439},[363,4711,738],{"class":394},[363,4713,4714],{"class":365,"line":682},[363,4715,523],{"class":394},[363,4717,4718],{"class":365,"line":699},[363,4719,381],{"emptyLinePlaceholder":380},[363,4721,4722,4724,4726,4729,4731,4733,4736,4738,4740,4742],{"class":365,"line":704},[363,4723,832],{"class":432},[363,4725,2080],{"class":387},[363,4727,4728],{"class":473}," search",[363,4730,477],{"class":394},[363,4732,3730],{"class":473},[363,4734,4735],{"class":3733}," text",[363,4737,3048],{"class":439},[363,4739,1758],{"class":373},[363,4741,489],{"class":394},[363,4743,4744],{"class":394}," {}\n",[363,4746,4747],{"class":365,"line":717},[363,4748,529],{"class":394},[179,4750,4751,4752,4754,4755,4757,4758,4760,4761,4763,4764,4766,4767,335],{},"目标系统早于 iOS 17 时，让这个类遵守 ",[198,4753,204],{},"，SwiftUI 用 ",[198,4756,264],{}," 直接读 ",[198,4759,486],{},"。iOS 17 的新页面用 ",[198,4762,200],{},"。它给 SwiftUI 跟踪属性，没有 ",[198,4765,542],{}," 这种发布者。UIKit 要订阅字段，继续用 ",[198,4768,308],{},[327,4770,3989],{"id":4771},"rxswift",[179,4773,4774,4776,4777,4780,4781,4784],{},[198,4775,811],{}," 表示以后还会来的值。",[198,4778,4779],{},"BehaviorRelay"," 记住当前值，后来的订阅者马上拿到它。订阅放进 ",[198,4782,4783],{},"DisposeBag","，控制器释放时一起取消。",[215,4786,4789],{"className":356,"code":4787,"filename":4788,"language":359,"meta":221,"style":221},"import RxRelay\nimport RxSwift\n\nfinal class RxCounterViewModel {\n    let count = BehaviorRelay(value: 0) \u002F\u002F 记住当前值，新订阅者马上能拿到\n\n    func increment() {\n        count.accept(count.value + 1) \u002F\u002F 用 accept 发出下一个值\n    }\n}\n","RxCounterViewModel.swift",[198,4790,4791,4798,4805,4809,4820,4845,4849,4859,4882,4886],{"__ignoreMap":221},[363,4792,4793,4795],{"class":365,"line":366},[363,4794,370],{"class":369},[363,4796,4797],{"class":373}," RxRelay\n",[363,4799,4800,4802],{"class":365,"line":377},[363,4801,370],{"class":369},[363,4803,4804],{"class":373}," RxSwift\n",[363,4806,4807],{"class":365,"line":384},[363,4808,381],{"emptyLinePlaceholder":380},[363,4810,4811,4813,4815,4818],{"class":365,"line":404},[363,4812,819],{"class":387},[363,4814,822],{"class":387},[363,4816,4817],{"class":373}," RxCounterViewModel",[363,4819,496],{"class":394},[363,4821,4822,4824,4826,4828,4831,4833,4836,4838,4840,4842],{"class":365,"line":411},[363,4823,1034],{"class":432},[363,4825,440],{"class":439},[363,4827,443],{"class":394},[363,4829,4830],{"class":473}," BehaviorRelay",[363,4832,477],{"class":394},[363,4834,4835],{"class":473},"value",[363,4837,395],{"class":394},[363,4839,848],{"class":432},[363,4841,489],{"class":394},[363,4843,4844],{"class":407}," \u002F\u002F 记住当前值，新订阅者马上能拿到\n",[363,4846,4847],{"class":365,"line":417},[363,4848,381],{"emptyLinePlaceholder":380},[363,4850,4851,4853,4855,4857],{"class":365,"line":423},[363,4852,860],{"class":387},[363,4854,863],{"class":473},[363,4856,866],{"class":394},[363,4858,496],{"class":394},[363,4860,4861,4864,4867,4869,4872,4875,4877,4879],{"class":365,"line":449},[363,4862,4863],{"class":439},"        count.",[363,4865,4866],{"class":473},"accept",[363,4868,477],{"class":394},[363,4870,4871],{"class":439},"count.value ",[363,4873,4874],{"class":394},"+",[363,4876,508],{"class":432},[363,4878,489],{"class":394},[363,4880,4881],{"class":407}," \u002F\u002F 用 accept 发出下一个值\n",[363,4883,4884],{"class":365,"line":454},[363,4885,523],{"class":394},[363,4887,4888],{"class":365,"line":470},[363,4889,529],{"class":394},[179,4891,4892,4893,4895],{},"控制器里把流送到标签。这和 Combine 的 ",[198,4894,3611],{}," 是同一类事。",[215,4897,4900],{"className":356,"code":4898,"filename":4899,"language":359,"meta":221,"style":221},"import RxSwift\n\nviewModel.count\n    .observe(on: MainScheduler.instance)\n    .subscribe(onNext: { [weak self] count in\n        self?.label.text = \"\\(count)\"\n    })\n    .disposed(by: disposeBag) \u002F\u002F 控制器释放时取消订阅\n","RxCounterBind.swift",[198,4901,4902,4908,4912,4917,4936,4960,4981,4986],{"__ignoreMap":221},[363,4903,4904,4906],{"class":365,"line":366},[363,4905,370],{"class":369},[363,4907,4804],{"class":373},[363,4909,4910],{"class":365,"line":377},[363,4911,381],{"emptyLinePlaceholder":380},[363,4913,4914],{"class":365,"line":384},[363,4915,4916],{"class":439},"viewModel.count\n",[363,4918,4919,4922,4925,4927,4929,4931,4934],{"class":365,"line":404},[363,4920,4921],{"class":439},"    .",[363,4923,4924],{"class":473},"observe",[363,4926,477],{"class":394},[363,4928,4367],{"class":473},[363,4930,395],{"class":394},[363,4932,4933],{"class":439}," MainScheduler.instance",[363,4935,738],{"class":394},[363,4937,4938,4940,4943,4945,4948,4950,4952,4954,4956,4958],{"class":365,"line":411},[363,4939,4921],{"class":439},[363,4941,4942],{"class":473},"subscribe",[363,4944,477],{"class":394},[363,4946,4947],{"class":473},"onNext",[363,4949,395],{"class":394},[363,4951,640],{"class":394},[363,4953,2782],{"class":439},[363,4955,2785],{"class":432},[363,4957,4392],{"class":439},[363,4959,2791],{"class":369},[363,4961,4962,4965,4967,4969,4971,4973,4975,4977,4979],{"class":365,"line":417},[363,4963,4964],{"class":439},"        self",[363,4966,2799],{"class":394},[363,4968,4404],{"class":439},[363,4970,443],{"class":394},[363,4972,2400],{"class":394},[363,4974,483],{"class":394},[363,4976,486],{"class":439},[363,4978,489],{"class":394},[363,4980,2409],{"class":394},[363,4982,4983],{"class":365,"line":423},[363,4984,4985],{"class":394},"    })\n",[363,4987,4988,4990,4993,4995,4998,5000,5003,5005],{"class":365,"line":449},[363,4989,4921],{"class":439},[363,4991,4992],{"class":473},"disposed",[363,4994,477],{"class":394},[363,4996,4997],{"class":473},"by",[363,4999,395],{"class":394},[363,5001,5002],{"class":439}," disposeBag",[363,5004,489],{"class":394},[363,5006,5007],{"class":407}," \u002F\u002F 控制器释放时取消订阅\n",[179,5009,5010,5011,5014,5015,5017],{},"状态仍在 ViewModel 里。Rx 负责运送值，不负责这一页有哪些字段。绑到界面的那一段可以用 ",[198,5012,5013],{},"Driver","：主线程、不把错误传到界面、共享一次订阅。网络那一段留在 ",[198,5016,811],{},"，失败在那里处理。",[327,5019,4007],{"id":5020},"reactorkit",[179,5022,5023,5024,5027,5028,335,5030,5032,5033,335,5036,5038,5039,1300,5041,5043,5044,5046,5047,335],{},"ReactorKit 把一屏收成 Reactor。视图把 ",[198,5025,5026],{},"Action"," 送进 ",[198,5029,2282],{},[198,5031,4019],{}," 做副作用，吐出 ",[198,5034,5035],{},"Mutation",[198,5037,3839],{}," 用旧 ",[198,5040,429],{},[198,5042,5035],{}," 算出新 ",[198,5045,429],{},"。视图只订阅 ",[198,5048,329],{},[215,5050,5053],{"className":356,"code":5051,"filename":5052,"language":359,"meta":221,"style":221},"import ReactorKit\nimport RxSwift\n\nfinal class CounterReactor: Reactor {\n    enum Action {\n        case increment\n    }\n\n    enum Mutation {\n        case bump\n    }\n\n    struct State {\n        var count = 0\n    }\n\n    let initialState = State()\n\n    func mutate(action: Action) -> Observable\u003CMutation> {\n        switch action {\n        case .increment:\n            return .just(.bump) \u002F\u002F 没有网络，动作直接变成一次变更\n        }\n    }\n\n    func reduce(state: State, mutation: Mutation) -> State {\n        var state = state\n        switch mutation {\n        case .bump:\n            state.count += 1 \u002F\u002F 加一写在纯函数里，连点两次各加一次\n        }\n        return state\n    }\n}\n","CounterReactor.swift",[198,5054,5055,5062,5068,5072,5088,5098,5106,5110,5114,5123,5130,5134,5138,5148,5158,5162,5166,5179,5183,5213,5222,5231,5251,5255,5259,5263,5291,5303,5312,5321,5333,5337,5344,5348],{"__ignoreMap":221},[363,5056,5057,5059],{"class":365,"line":366},[363,5058,370],{"class":369},[363,5060,5061],{"class":373}," ReactorKit\n",[363,5063,5064,5066],{"class":365,"line":377},[363,5065,370],{"class":369},[363,5067,4804],{"class":373},[363,5069,5070],{"class":365,"line":384},[363,5071,381],{"emptyLinePlaceholder":380},[363,5073,5074,5076,5078,5081,5083,5086],{"class":365,"line":404},[363,5075,819],{"class":387},[363,5077,822],{"class":387},[363,5079,5080],{"class":373}," CounterReactor",[363,5082,395],{"class":394},[363,5084,5085],{"class":373}," Reactor ",[363,5087,401],{"class":394},[363,5089,5090,5093,5096],{"class":365,"line":411},[363,5091,5092],{"class":387},"    enum",[363,5094,5095],{"class":373}," Action",[363,5097,496],{"class":394},[363,5099,5100,5103],{"class":365,"line":417},[363,5101,5102],{"class":387},"        case",[363,5104,5105],{"class":439}," increment\n",[363,5107,5108],{"class":365,"line":423},[363,5109,523],{"class":394},[363,5111,5112],{"class":365,"line":449},[363,5113,381],{"emptyLinePlaceholder":380},[363,5115,5116,5118,5121],{"class":365,"line":454},[363,5117,5092],{"class":387},[363,5119,5120],{"class":373}," Mutation",[363,5122,496],{"class":394},[363,5124,5125,5127],{"class":365,"line":470},[363,5126,5102],{"class":387},[363,5128,5129],{"class":439}," bump\n",[363,5131,5132],{"class":365,"line":499},[363,5133,523],{"class":394},[363,5135,5136],{"class":365,"line":514},[363,5137,381],{"emptyLinePlaceholder":380},[363,5139,5140,5143,5146],{"class":365,"line":520},[363,5141,5142],{"class":387},"    struct",[363,5144,5145],{"class":373}," State",[363,5147,496],{"class":394},[363,5149,5150,5152,5154,5156],{"class":365,"line":526},[363,5151,3064],{"class":432},[363,5153,440],{"class":439},[363,5155,443],{"class":394},[363,5157,446],{"class":432},[363,5159,5160],{"class":365,"line":682},[363,5161,523],{"class":394},[363,5163,5164],{"class":365,"line":699},[363,5165,381],{"emptyLinePlaceholder":380},[363,5167,5168,5170,5173,5175,5177],{"class":365,"line":704},[363,5169,1034],{"class":432},[363,5171,5172],{"class":439}," initialState ",[363,5174,443],{"class":394},[363,5176,5145],{"class":473},[363,5178,933],{"class":394},[363,5180,5181],{"class":365,"line":717},[363,5182,381],{"emptyLinePlaceholder":380},[363,5184,5185,5187,5190,5192,5194,5197,5199,5201,5204,5206,5208,5211],{"class":365,"line":723},[363,5186,860],{"class":387},[363,5188,5189],{"class":473}," mutate",[363,5191,477],{"class":394},[363,5193,2282],{"class":2525},[363,5195,5196],{"class":439},": Action",[363,5198,489],{"class":394},[363,5200,2500],{"class":394},[363,5202,5203],{"class":439}," Observable",[363,5205,3024],{"class":394},[363,5207,5035],{"class":439},[363,5209,5210],{"class":394},">",[363,5212,496],{"class":394},[363,5214,5215,5218,5220],{"class":365,"line":741},[363,5216,5217],{"class":369},"        switch",[363,5219,3762],{"class":439},[363,5221,401],{"class":394},[363,5223,5224,5226,5229],{"class":365,"line":746},[363,5225,5102],{"class":369},[363,5227,5228],{"class":439}," .increment",[363,5230,3774],{"class":394},[363,5232,5233,5236,5238,5241,5243,5246,5248],{"class":365,"line":990},[363,5234,5235],{"class":369},"            return",[363,5237,3795],{"class":439},[363,5239,5240],{"class":473},"just",[363,5242,477],{"class":394},[363,5244,5245],{"class":439},".bump",[363,5247,489],{"class":394},[363,5249,5250],{"class":407}," \u002F\u002F 没有网络，动作直接变成一次变更\n",[363,5252,5253],{"class":365,"line":995},[363,5254,517],{"class":394},[363,5256,5257],{"class":365,"line":1000},[363,5258,523],{"class":394},[363,5260,5261],{"class":365,"line":1005},[363,5262,381],{"emptyLinePlaceholder":380},[363,5264,5265,5267,5269,5271,5273,5276,5279,5282,5284,5286,5289],{"class":365,"line":1019},[363,5266,860],{"class":387},[363,5268,3725],{"class":473},[363,5270,477],{"class":394},[363,5272,329],{"class":2525},[363,5274,5275],{"class":439},": State, ",[363,5277,5278],{"class":2525},"mutation",[363,5280,5281],{"class":439},": Mutation",[363,5283,489],{"class":394},[363,5285,2500],{"class":394},[363,5287,5288],{"class":439}," State ",[363,5290,401],{"class":394},[363,5292,5293,5295,5298,5300],{"class":365,"line":1025},[363,5294,3064],{"class":432},[363,5296,5297],{"class":439}," state ",[363,5299,443],{"class":394},[363,5301,5302],{"class":439}," state\n",[363,5304,5305,5307,5310],{"class":365,"line":1031},[363,5306,5217],{"class":369},[363,5308,5309],{"class":439}," mutation ",[363,5311,401],{"class":394},[363,5313,5314,5316,5319],{"class":365,"line":1040},[363,5315,5102],{"class":369},[363,5317,5318],{"class":439}," .bump",[363,5320,3774],{"class":394},[363,5322,5323,5326,5328,5330],{"class":365,"line":1045},[363,5324,5325],{"class":439},"            state.count ",[363,5327,505],{"class":394},[363,5329,508],{"class":432},[363,5331,5332],{"class":407}," \u002F\u002F 加一写在纯函数里，连点两次各加一次\n",[363,5334,5335],{"class":365,"line":1058},[363,5336,517],{"class":394},[363,5338,5339,5342],{"class":365,"line":1078},[363,5340,5341],{"class":369},"        return",[363,5343,5302],{"class":439},[363,5345,5346],{"class":365,"line":1083},[363,5347,523],{"class":394},[363,5349,5351],{"class":365,"line":5350},34,[363,5352,529],{"class":394},[179,5354,5355,5356,5358,5359,5361,5362,5364,5365,3159,5367,5369],{},"加一没有副作用，",[198,5357,4019],{}," 只把动作变成变更，加一写在 ",[198,5360,3839],{}," 里。这样连续点两次，每次都在当时的状态上加。拉网络时，",[198,5363,4019],{}," 返回一个稍后才发出结果的 ",[198,5366,811],{},[198,5368,3839],{}," 里只赋值。",[215,5371,5374],{"className":356,"code":5372,"filename":5373,"language":359,"meta":221,"style":221},"import RxCocoa\nimport RxSwift\n\nbutton.rx.tap\n    .map { CounterReactor.Action.increment } \u002F\u002F 点击变成动作\n    .bind(to: reactor.action)\n    .disposed(by: disposeBag)\n\nreactor.state\n    .map(\\.count)\n    .distinctUntilChanged()\n    .observe(on: MainScheduler.instance)\n    .subscribe(onNext: { [weak self] count in\n        self?.label.text = \"\\(count)\"\n    })\n    .disposed(by: disposeBag)\n","CounterReactorBind.swift",[198,5375,5376,5383,5389,5393,5398,5415,5434,5450,5454,5459,5472,5481,5497,5519,5539,5543],{"__ignoreMap":221},[363,5377,5378,5380],{"class":365,"line":366},[363,5379,370],{"class":369},[363,5381,5382],{"class":373}," RxCocoa\n",[363,5384,5385,5387],{"class":365,"line":377},[363,5386,370],{"class":369},[363,5388,4804],{"class":373},[363,5390,5391],{"class":365,"line":384},[363,5392,381],{"emptyLinePlaceholder":380},[363,5394,5395],{"class":365,"line":404},[363,5396,5397],{"class":439},"button.rx.tap\n",[363,5399,5400,5402,5404,5406,5409,5412],{"class":365,"line":411},[363,5401,4921],{"class":439},[363,5403,3109],{"class":473},[363,5405,640],{"class":394},[363,5407,5408],{"class":439}," CounterReactor.Action.increment ",[363,5410,5411],{"class":394},"}",[363,5413,5414],{"class":407}," \u002F\u002F 点击变成动作\n",[363,5416,5417,5419,5422,5424,5427,5429,5432],{"class":365,"line":417},[363,5418,4921],{"class":439},[363,5420,5421],{"class":473},"bind",[363,5423,477],{"class":394},[363,5425,5426],{"class":473},"to",[363,5428,395],{"class":394},[363,5430,5431],{"class":439}," reactor.action",[363,5433,738],{"class":394},[363,5435,5436,5438,5440,5442,5444,5446,5448],{"class":365,"line":423},[363,5437,4921],{"class":439},[363,5439,4992],{"class":473},[363,5441,477],{"class":394},[363,5443,4997],{"class":473},[363,5445,395],{"class":394},[363,5447,5002],{"class":439},[363,5449,738],{"class":394},[363,5451,5452],{"class":365,"line":449},[363,5453,381],{"emptyLinePlaceholder":380},[363,5455,5456],{"class":365,"line":454},[363,5457,5458],{"class":439},"reactor.state\n",[363,5460,5461,5463,5465,5467,5470],{"class":365,"line":470},[363,5462,4921],{"class":439},[363,5464,3109],{"class":473},[363,5466,477],{"class":394},[363,5468,5469],{"class":439},"\\.count",[363,5471,738],{"class":394},[363,5473,5474,5476,5479],{"class":365,"line":499},[363,5475,4921],{"class":439},[363,5477,5478],{"class":473},"distinctUntilChanged",[363,5480,933],{"class":394},[363,5482,5483,5485,5487,5489,5491,5493,5495],{"class":365,"line":514},[363,5484,4921],{"class":439},[363,5486,4924],{"class":473},[363,5488,477],{"class":394},[363,5490,4367],{"class":473},[363,5492,395],{"class":394},[363,5494,4933],{"class":439},[363,5496,738],{"class":394},[363,5498,5499,5501,5503,5505,5507,5509,5511,5513,5515,5517],{"class":365,"line":520},[363,5500,4921],{"class":439},[363,5502,4942],{"class":473},[363,5504,477],{"class":394},[363,5506,4947],{"class":473},[363,5508,395],{"class":394},[363,5510,640],{"class":394},[363,5512,2782],{"class":439},[363,5514,2785],{"class":432},[363,5516,4392],{"class":439},[363,5518,2791],{"class":369},[363,5520,5521,5523,5525,5527,5529,5531,5533,5535,5537],{"class":365,"line":526},[363,5522,4964],{"class":439},[363,5524,2799],{"class":394},[363,5526,4404],{"class":439},[363,5528,443],{"class":394},[363,5530,2400],{"class":394},[363,5532,483],{"class":394},[363,5534,486],{"class":439},[363,5536,489],{"class":394},[363,5538,2409],{"class":394},[363,5540,5541],{"class":365,"line":682},[363,5542,4985],{"class":394},[363,5544,5545,5547,5549,5551,5553,5555,5557],{"class":365,"line":699},[363,5546,4921],{"class":439},[363,5548,4992],{"class":473},[363,5550,477],{"class":394},[363,5552,4997],{"class":473},[363,5554,395],{"class":394},[363,5556,5002],{"class":439},[363,5558,738],{"class":394},[179,5560,5561,5564,5565,5568,5569,5572,5573,335],{},[198,5562,5563],{},"label.rx.text"," 也能接这条流。它的元素类型是 ",[198,5566,5567],{},"String?","，和 ",[198,5570,5571],{},"Binder"," 对上再 ",[198,5574,5421],{},[179,5576,5577,5578,335,5581,5583,5584,5587,5588,5591,5592,5595,5596,5599,5600,3155,5602,5605,5606,335],{},"SwiftUI 用同一份 Reactor，模块是 ",[198,5579,5580],{},"ReactorKitSwiftUI",[198,5582,429],{}," 标 ",[198,5585,5586],{},"@ObservableState","（宏在 ",[198,5589,5590],{},"ReactorKitObservation","），视图拿 ",[198,5593,5594],{},"ObservedReactor","，直接读字段、",[198,5597,5598],{},"send"," 动作。要双向绑输入框时，让 ",[198,5601,5026],{},[198,5603,5604],{},"BindableAction","，视图用 ",[198,5607,5608],{},"@ReactorBindable",[179,5610,5611],{},"工程里已经有 RxSwift，每屏又要把副作用和纯计算分开，再用 ReactorKit。",[327,5613,4024],{"id":5614},"reswift",[179,5616,5617,5618,5621,5622,5625,5626,5628],{},"一个 ",[198,5619,5620],{},"Store"," 拿着整个 App 的状态。各处 ",[198,5623,5624],{},"dispatch"," 动作。Reducer 是纯函数。副作用放 Middleware，做完再 ",[198,5627,5624],{}," 下一个动作。",[215,5630,5633],{"className":356,"code":5631,"filename":5632,"language":359,"meta":221,"style":221},"import ReSwift\n\nstruct AppState: StateType {\n    var count = 0\n}\n\nstruct Increment: Action {}\n\nfunc counterReducer(action: Action, state: AppState?) -> AppState {\n    var state = state ?? AppState() \u002F\u002F 第一次进来还没有状态\n    switch action {\n    case is Increment:\n        state.count += 1\n    default:\n        break \u002F\u002F 别的页面的动作从这里走掉\n    }\n    return state\n}\n\nlet store = Store\u003CAppState>(reducer: counterReducer, state: nil)\nstore.dispatch(Increment())\n","ReSwiftCounter.swift",[198,5634,5635,5642,5646,5660,5670,5674,5678,5693,5697,5726,5746,5754,5765,5773,5780,5788,5792,5799,5803,5807,5841],{"__ignoreMap":221},[363,5636,5637,5639],{"class":365,"line":366},[363,5638,370],{"class":369},[363,5640,5641],{"class":373}," ReSwift\n",[363,5643,5644],{"class":365,"line":377},[363,5645,381],{"emptyLinePlaceholder":380},[363,5647,5648,5650,5653,5655,5658],{"class":365,"line":384},[363,5649,388],{"class":387},[363,5651,5652],{"class":373}," AppState",[363,5654,395],{"class":394},[363,5656,5657],{"class":373}," StateType ",[363,5659,401],{"class":394},[363,5661,5662,5664,5666,5668],{"class":365,"line":404},[363,5663,457],{"class":432},[363,5665,440],{"class":439},[363,5667,443],{"class":394},[363,5669,446],{"class":432},[363,5671,5672],{"class":365,"line":411},[363,5673,529],{"class":394},[363,5675,5676],{"class":365,"line":417},[363,5677,381],{"emptyLinePlaceholder":380},[363,5679,5680,5682,5685,5687,5690],{"class":365,"line":423},[363,5681,388],{"class":387},[363,5683,5684],{"class":373}," Increment",[363,5686,395],{"class":394},[363,5688,5689],{"class":373}," Action ",[363,5691,5692],{"class":394},"{}\n",[363,5694,5695],{"class":365,"line":449},[363,5696,381],{"emptyLinePlaceholder":380},[363,5698,5699,5701,5704,5706,5708,5711,5713,5716,5719,5721,5724],{"class":365,"line":454},[363,5700,3722],{"class":387},[363,5702,5703],{"class":473}," counterReducer",[363,5705,477],{"class":394},[363,5707,2282],{"class":2525},[363,5709,5710],{"class":439},": Action, ",[363,5712,329],{"class":2525},[363,5714,5715],{"class":439},": AppState",[363,5717,5718],{"class":394},"?)",[363,5720,2500],{"class":394},[363,5722,5723],{"class":439}," AppState ",[363,5725,401],{"class":394},[363,5727,5728,5730,5732,5734,5736,5739,5741,5743],{"class":365,"line":470},[363,5729,457],{"class":432},[363,5731,5297],{"class":439},[363,5733,443],{"class":394},[363,5735,5297],{"class":439},[363,5737,5738],{"class":394},"??",[363,5740,5652],{"class":473},[363,5742,866],{"class":394},[363,5744,5745],{"class":407}," \u002F\u002F 第一次进来还没有状态\n",[363,5747,5748,5750,5752],{"class":365,"line":499},[363,5749,3759],{"class":369},[363,5751,3762],{"class":439},[363,5753,401],{"class":394},[363,5755,5756,5758,5761,5763],{"class":365,"line":514},[363,5757,2986],{"class":369},[363,5759,5760],{"class":394}," is",[363,5762,5684],{"class":439},[363,5764,3774],{"class":394},[363,5766,5767,5769,5771],{"class":365,"line":520},[363,5768,3779],{"class":439},[363,5770,505],{"class":394},[363,5772,878],{"class":432},[363,5774,5775,5778],{"class":365,"line":526},[363,5776,5777],{"class":369},"    default",[363,5779,3774],{"class":394},[363,5781,5782,5785],{"class":365,"line":682},[363,5783,5784],{"class":369},"        break",[363,5786,5787],{"class":407}," \u002F\u002F 别的页面的动作从这里走掉\n",[363,5789,5790],{"class":365,"line":699},[363,5791,523],{"class":394},[363,5793,5794,5797],{"class":365,"line":704},[363,5795,5796],{"class":369},"    return",[363,5798,5302],{"class":439},[363,5800,5801],{"class":365,"line":717},[363,5802,529],{"class":394},[363,5804,5805],{"class":365,"line":723},[363,5806,381],{"emptyLinePlaceholder":380},[363,5808,5809,5811,5814,5816,5819,5821,5824,5827,5830,5832,5835,5837,5839],{"class":365,"line":741},[363,5810,773],{"class":432},[363,5812,5813],{"class":439}," store ",[363,5815,443],{"class":394},[363,5817,5818],{"class":439}," Store",[363,5820,3024],{"class":394},[363,5822,5823],{"class":439},"AppState",[363,5825,5826],{"class":394},">(",[363,5828,5829],{"class":473},"reducer",[363,5831,395],{"class":394},[363,5833,5834],{"class":439}," counterReducer, ",[363,5836,329],{"class":473},[363,5838,395],{"class":394},[363,5840,4284],{"class":394},[363,5842,5843,5846,5848,5850,5853],{"class":365,"line":746},[363,5844,5845],{"class":439},"store.",[363,5847,5624],{"class":473},[363,5849,477],{"class":394},[363,5851,5852],{"class":473},"Increment",[363,5854,5855],{"class":394},"())\n",[179,5857,5858,5859,3574,5862,5865,5866,335,5868,5865,5871,5873,5874,335],{},"页面遵守 ",[198,5860,5861],{},"StoreSubscriber",[198,5863,5864],{},"newState(state:)"," 里 ",[198,5867,2822],{},[198,5869,5870],{},"viewWillAppear",[198,5872,4942],{},"，离开时 ",[198,5875,5876],{},"unsubscribe",[179,5878,5879,5880,335],{},"状态只有一棵树，动作可以打日志、可以重放。一个计数器的动作和设置页的动作住在一起，模块靠自己划状态的分支。新代码里这条路我写得少，一屏一棵状态树的位置多半是 TCA，或者就是上面那个自己写的 ",[198,5881,3839],{},[327,5883,4038],{"id":5884},"tca",[179,5886,5887,5888,5890,5891,5893,5894,5897,5898,335,5901,5903,5904,5906],{},"TCA 把一屏叫做 Feature。",[198,5889,429],{}," 是值，",[198,5892,5026],{}," 是枚举。",[198,5895,5896],{},"Reducer"," 根据动作改状态，并返回 ",[198,5899,5900],{},"Effect",[198,5902,5620],{}," 是运行时：视图 ",[198,5905,5598],{}," 动作，读状态来画。",[215,5908,5911],{"className":356,"code":5909,"filename":5910,"language":359,"meta":221,"style":221},"import ComposableArchitecture\n\nenum NumberAPI {\n    static func fact(for count: Int) async -> String {\n        \"关于 \\(count) 的说明\"\n    }\n}\n\n@Reducer\nstruct CounterFeature {\n    @ObservableState\n    struct State: Equatable {\n        var count = 0\n        var fact: String?\n    }\n\n    enum Action {\n        case incrementTapped\n        case factTapped\n        case factResponse(String)\n    }\n\n    var body: some Reducer\u003CState, Action> {\n        Reduce { state, action in\n            switch action {\n            case .incrementTapped:\n                state.count += 1\n                return .none \u002F\u002F 没有副作用\n\n            case .factTapped:\n                return .run { [count = state.count] send in\n                    \u002F\u002F 请求在 Effect 里。先抄出 count，闭包不抓整个 state\n                    let fact = await NumberAPI.fact(for: count)\n                    await send(.factResponse(fact)) \u002F\u002F 结果再变回一个动作\n                }\n\n            case let .factResponse(fact):\n                state.fact = fact\n                return .none\n            }\n        }\n    }\n}\n","CounterFeature.swift",[198,5912,5913,5920,5924,5933,5965,5984,5988,5992,5996,6003,6012,6019,6031,6041,6051,6055,6059,6067,6073,6080,6093,6097,6101,6121,6133,6142,6151,6160,6171,6175,6184,6205,6210,6239,6263,6269,6274,6291,6302,6310,6315,6320,6325],{"__ignoreMap":221},[363,5914,5915,5917],{"class":365,"line":366},[363,5916,370],{"class":369},[363,5918,5919],{"class":373}," ComposableArchitecture\n",[363,5921,5922],{"class":365,"line":377},[363,5923,381],{"emptyLinePlaceholder":380},[363,5925,5926,5928,5931],{"class":365,"line":384},[363,5927,2971],{"class":387},[363,5929,5930],{"class":373}," NumberAPI",[363,5932,496],{"class":394},[363,5934,5935,5938,5940,5943,5945,5947,5949,5951,5953,5955,5958,5960,5963],{"class":365,"line":404},[363,5936,5937],{"class":387},"    static",[363,5939,2080],{"class":387},[363,5941,5942],{"class":473}," fact",[363,5944,477],{"class":394},[363,5946,2260],{"class":473},[363,5948,3557],{"class":3733},[363,5950,3048],{"class":439},[363,5952,1752],{"class":373},[363,5954,489],{"class":394},[363,5956,5957],{"class":387}," async",[363,5959,2500],{"class":394},[363,5961,5962],{"class":373}," String",[363,5964,496],{"class":394},[363,5966,5967,5970,5973,5975,5977,5979,5982],{"class":365,"line":411},[363,5968,5969],{"class":394},"        \"",[363,5971,5972],{"class":1234},"关于 ",[363,5974,483],{"class":394},[363,5976,486],{"class":439},[363,5978,489],{"class":394},[363,5980,5981],{"class":1234}," 的说明",[363,5983,2409],{"class":394},[363,5985,5986],{"class":365,"line":417},[363,5987,523],{"class":394},[363,5989,5990],{"class":365,"line":423},[363,5991,529],{"class":394},[363,5993,5994],{"class":365,"line":449},[363,5995,381],{"emptyLinePlaceholder":380},[363,5997,5998,6000],{"class":365,"line":454},[363,5999,801],{"class":394},[363,6001,6002],{"class":387},"Reducer\n",[363,6004,6005,6007,6010],{"class":365,"line":470},[363,6006,388],{"class":387},[363,6008,6009],{"class":373}," CounterFeature",[363,6011,496],{"class":394},[363,6013,6014,6016],{"class":365,"line":499},[363,6015,426],{"class":394},[363,6017,6018],{"class":387},"ObservableState\n",[363,6020,6021,6023,6025,6027,6029],{"class":365,"line":514},[363,6022,5142],{"class":387},[363,6024,5145],{"class":373},[363,6026,395],{"class":394},[363,6028,3648],{"class":373},[363,6030,401],{"class":394},[363,6032,6033,6035,6037,6039],{"class":365,"line":520},[363,6034,3064],{"class":432},[363,6036,440],{"class":439},[363,6038,443],{"class":394},[363,6040,446],{"class":432},[363,6042,6043,6045,6047,6049],{"class":365,"line":526},[363,6044,3064],{"class":432},[363,6046,3667],{"class":439},[363,6048,1758],{"class":373},[363,6050,3672],{"class":394},[363,6052,6053],{"class":365,"line":682},[363,6054,523],{"class":394},[363,6056,6057],{"class":365,"line":699},[363,6058,381],{"emptyLinePlaceholder":380},[363,6060,6061,6063,6065],{"class":365,"line":704},[363,6062,5092],{"class":387},[363,6064,5095],{"class":373},[363,6066,496],{"class":394},[363,6068,6069,6071],{"class":365,"line":717},[363,6070,5102],{"class":387},[363,6072,3696],{"class":439},[363,6074,6075,6077],{"class":365,"line":723},[363,6076,5102],{"class":387},[363,6078,6079],{"class":439}," factTapped\n",[363,6081,6082,6084,6087,6089,6091],{"class":365,"line":741},[363,6083,5102],{"class":387},[363,6085,6086],{"class":439}," factResponse",[363,6088,477],{"class":394},[363,6090,1758],{"class":373},[363,6092,738],{"class":394},[363,6094,6095],{"class":365,"line":746},[363,6096,523],{"class":394},[363,6098,6099],{"class":365,"line":990},[363,6100,381],{"emptyLinePlaceholder":380},[363,6102,6103,6105,6107,6109,6112,6114,6117,6119],{"class":365,"line":995},[363,6104,457],{"class":432},[363,6106,460],{"class":439},[363,6108,463],{"class":432},[363,6110,6111],{"class":439}," Reducer",[363,6113,3024],{"class":394},[363,6115,6116],{"class":439},"State, Action",[363,6118,5210],{"class":394},[363,6120,496],{"class":394},[363,6122,6123,6126,6128,6131],{"class":365,"line":1000},[363,6124,6125],{"class":473},"        Reduce",[363,6127,640],{"class":394},[363,6129,6130],{"class":439}," state, action ",[363,6132,2791],{"class":369},[363,6134,6135,6138,6140],{"class":365,"line":1005},[363,6136,6137],{"class":369},"            switch",[363,6139,3762],{"class":439},[363,6141,401],{"class":394},[363,6143,6144,6147,6149],{"class":365,"line":1019},[363,6145,6146],{"class":369},"            case",[363,6148,3771],{"class":439},[363,6150,3774],{"class":394},[363,6152,6153,6156,6158],{"class":365,"line":1025},[363,6154,6155],{"class":439},"                state.count ",[363,6157,505],{"class":394},[363,6159,878],{"class":432},[363,6161,6162,6165,6168],{"class":365,"line":1031},[363,6163,6164],{"class":369},"                return",[363,6166,6167],{"class":439}," .none ",[363,6169,6170],{"class":407},"\u002F\u002F 没有副作用\n",[363,6172,6173],{"class":365,"line":1040},[363,6174,381],{"emptyLinePlaceholder":380},[363,6176,6177,6179,6182],{"class":365,"line":1045},[363,6178,6146],{"class":369},[363,6180,6181],{"class":439}," .factTapped",[363,6183,3774],{"class":394},[363,6185,6186,6188,6190,6193,6195,6198,6200,6203],{"class":365,"line":1058},[363,6187,6164],{"class":369},[363,6189,3795],{"class":439},[363,6191,6192],{"class":473},"run",[363,6194,640],{"class":394},[363,6196,6197],{"class":439}," [count ",[363,6199,443],{"class":394},[363,6201,6202],{"class":439}," state.count] send ",[363,6204,2791],{"class":369},[363,6206,6207],{"class":365,"line":1078},[363,6208,6209],{"class":407},"                    \u002F\u002F 请求在 Effect 里。先抄出 count，闭包不抓整个 state\n",[363,6211,6212,6215,6218,6220,6223,6226,6229,6231,6233,6235,6237],{"class":365,"line":1083},[363,6213,6214],{"class":432},"                    let",[363,6216,6217],{"class":439}," fact ",[363,6219,443],{"class":394},[363,6221,6222],{"class":369}," await",[363,6224,6225],{"class":439}," NumberAPI.",[363,6227,6228],{"class":473},"fact",[363,6230,477],{"class":394},[363,6232,2260],{"class":473},[363,6234,395],{"class":394},[363,6236,3557],{"class":439},[363,6238,738],{"class":394},[363,6240,6241,6244,6247,6249,6251,6254,6256,6258,6260],{"class":365,"line":5350},[363,6242,6243],{"class":369},"                    await",[363,6245,6246],{"class":473}," send",[363,6248,477],{"class":394},[363,6250,2819],{"class":439},[363,6252,6253],{"class":473},"factResponse",[363,6255,477],{"class":394},[363,6257,6228],{"class":439},[363,6259,3117],{"class":394},[363,6261,6262],{"class":407}," \u002F\u002F 结果再变回一个动作\n",[363,6264,6266],{"class":365,"line":6265},35,[363,6267,6268],{"class":394},"                }\n",[363,6270,6272],{"class":365,"line":6271},36,[363,6273,381],{"emptyLinePlaceholder":380},[363,6275,6277,6279,6281,6283,6285,6287,6289],{"class":365,"line":6276},37,[363,6278,6146],{"class":369},[363,6280,2058],{"class":432},[363,6282,3795],{"class":439},[363,6284,6253],{"class":473},[363,6286,477],{"class":394},[363,6288,6228],{"class":439},[363,6290,3805],{"class":394},[363,6292,6294,6297,6299],{"class":365,"line":6293},38,[363,6295,6296],{"class":439},"                state.fact ",[363,6298,443],{"class":394},[363,6300,6301],{"class":439}," fact\n",[363,6303,6305,6307],{"class":365,"line":6304},39,[363,6306,6164],{"class":369},[363,6308,6309],{"class":439}," .none\n",[363,6311,6313],{"class":365,"line":6312},40,[363,6314,4421],{"class":394},[363,6316,6318],{"class":365,"line":6317},41,[363,6319,517],{"class":394},[363,6321,6323],{"class":365,"line":6322},42,[363,6324,523],{"class":394},[363,6326,6328],{"class":365,"line":6327},43,[363,6329,529],{"class":394},[179,6331,6332,6335,6336,6338,6339,6342],{},[198,6333,6334],{},"NumberAPI.fact"," 只代替那次请求。真正的接口会失败，那时再 ",[198,6337,5598],{}," 一个失败动作，Reducer 里写上错误字段。依赖用 TCA 的 ",[198,6340,6341],{},"@Dependency"," 注册之后，测试里可以换成立刻返回的实现。状态仍然只在 Reducer 里改。",[215,6344,6347],{"className":356,"code":6345,"filename":6346,"language":359,"meta":221,"style":221},"import ComposableArchitecture\nimport SwiftUI\n\nstruct CounterFeatureView: View {\n    let store: StoreOf\u003CCounterFeature>\n\n    var body: some View {\n        VStack {\n            Button(\"\\(store.count)\") {\n                store.send(.incrementTapped) \u002F\u002F 视图只发动作，不自己改 count\n            }\n            Button(\"说明\") {\n                store.send(.factTapped)\n            }\n            if let fact = store.fact {\n                Text(fact)\n            }\n        }\n    }\n}\n","CounterFeatureView.swift",[198,6348,6349,6355,6361,6365,6378,6393,6397,6409,6416,6438,6454,6458,6475,6488,6492,6508,6519,6523,6527,6531],{"__ignoreMap":221},[363,6350,6351,6353],{"class":365,"line":366},[363,6352,370],{"class":369},[363,6354,5919],{"class":373},[363,6356,6357,6359],{"class":365,"line":377},[363,6358,370],{"class":369},[363,6360,374],{"class":373},[363,6362,6363],{"class":365,"line":384},[363,6364,381],{"emptyLinePlaceholder":380},[363,6366,6367,6369,6372,6374,6376],{"class":365,"line":404},[363,6368,388],{"class":387},[363,6370,6371],{"class":373}," CounterFeatureView",[363,6373,395],{"class":394},[363,6375,398],{"class":373},[363,6377,401],{"class":394},[363,6379,6380,6382,6385,6387,6390],{"class":365,"line":411},[363,6381,1034],{"class":432},[363,6383,6384],{"class":439}," store: StoreOf",[363,6386,3024],{"class":394},[363,6388,6389],{"class":439},"CounterFeature",[363,6391,6392],{"class":394},">\n",[363,6394,6395],{"class":365,"line":417},[363,6396,381],{"emptyLinePlaceholder":380},[363,6398,6399,6401,6403,6405,6407],{"class":365,"line":423},[363,6400,457],{"class":432},[363,6402,460],{"class":439},[363,6404,463],{"class":432},[363,6406,398],{"class":439},[363,6408,401],{"class":394},[363,6410,6411,6414],{"class":365,"line":449},[363,6412,6413],{"class":473},"        VStack",[363,6415,496],{"class":394},[363,6417,6418,6421,6423,6425,6427,6430,6432,6434,6436],{"class":365,"line":454},[363,6419,6420],{"class":473},"            Button",[363,6422,477],{"class":394},[363,6424,480],{"class":394},[363,6426,483],{"class":394},[363,6428,6429],{"class":439},"store.count",[363,6431,489],{"class":394},[363,6433,480],{"class":394},[363,6435,489],{"class":394},[363,6437,496],{"class":394},[363,6439,6440,6443,6445,6447,6449,6451],{"class":365,"line":470},[363,6441,6442],{"class":439},"                store.",[363,6444,5598],{"class":473},[363,6446,477],{"class":394},[363,6448,3832],{"class":439},[363,6450,489],{"class":394},[363,6452,6453],{"class":407}," \u002F\u002F 视图只发动作，不自己改 count\n",[363,6455,6456],{"class":365,"line":499},[363,6457,4421],{"class":394},[363,6459,6460,6462,6464,6466,6469,6471,6473],{"class":365,"line":514},[363,6461,6420],{"class":473},[363,6463,477],{"class":394},[363,6465,480],{"class":394},[363,6467,6468],{"class":1234},"说明",[363,6470,480],{"class":394},[363,6472,489],{"class":394},[363,6474,496],{"class":394},[363,6476,6477,6479,6481,6483,6486],{"class":365,"line":520},[363,6478,6442],{"class":439},[363,6480,5598],{"class":473},[363,6482,477],{"class":394},[363,6484,6485],{"class":439},".factTapped",[363,6487,738],{"class":394},[363,6489,6490],{"class":365,"line":526},[363,6491,4421],{"class":394},[363,6493,6494,6497,6499,6501,6503,6506],{"class":365,"line":682},[363,6495,6496],{"class":369},"            if",[363,6498,2058],{"class":432},[363,6500,6217],{"class":439},[363,6502,443],{"class":394},[363,6504,6505],{"class":439}," store.fact ",[363,6507,401],{"class":394},[363,6509,6510,6513,6515,6517],{"class":365,"line":699},[363,6511,6512],{"class":473},"                Text",[363,6514,477],{"class":394},[363,6516,6228],{"class":439},[363,6518,738],{"class":394},[363,6520,6521],{"class":365,"line":704},[363,6522,4421],{"class":394},[363,6524,6525],{"class":365,"line":717},[363,6526,517],{"class":394},[363,6528,6529],{"class":365,"line":723},[363,6530,523],{"class":394},[363,6532,6533],{"class":365,"line":741},[363,6534,529],{"class":394},[179,6536,6537,6538,335],{},"创建处把初值和 Reducer 交给 ",[198,6539,5620],{},[215,6541,6544],{"className":356,"code":6542,"filename":6543,"language":359,"meta":221,"style":221},"CounterFeatureView(\n    store: Store(initialState: CounterFeature.State()) {\n        CounterFeature() \u002F\u002F 初值和 Reducer 交给 Store，视图只拿着它\n    }\n)\n","CounterFeatureHost.swift",[198,6545,6546,6554,6580,6590,6594],{"__ignoreMap":221},[363,6547,6548,6551],{"class":365,"line":366},[363,6549,6550],{"class":473},"CounterFeatureView",[363,6552,6553],{"class":394},"(\n",[363,6555,6556,6559,6561,6563,6565,6568,6570,6573,6575,6578],{"class":365,"line":377},[363,6557,6558],{"class":473},"    store",[363,6560,395],{"class":394},[363,6562,5818],{"class":473},[363,6564,477],{"class":394},[363,6566,6567],{"class":473},"initialState",[363,6569,395],{"class":394},[363,6571,6572],{"class":439}," CounterFeature.",[363,6574,429],{"class":473},[363,6576,6577],{"class":394},"())",[363,6579,496],{"class":394},[363,6581,6582,6585,6587],{"class":365,"line":384},[363,6583,6584],{"class":473},"        CounterFeature",[363,6586,866],{"class":394},[363,6588,6589],{"class":407}," \u002F\u002F 初值和 Reducer 交给 Store，视图只拿着它\n",[363,6591,6592],{"class":365,"line":404},[363,6593,523],{"class":394},[363,6595,6596],{"class":365,"line":411},[363,6597,738],{"class":394},[179,6599,6600,6602,6603,6605,6606,6608],{},[198,6601,5586],{}," 让视图只跟踪自己读过的字段，和 SwiftUI 的 ",[198,6604,200],{}," 是同一类跟踪。TCA 自带的观察可以在 iOS 17 之前用。系统的 ",[198,6607,200],{}," 仍然要 iOS 17。",[179,6610,6611,6612,6614,6615,6617,6618,5568,6620,6622,6623,6625],{},"子功能的 ",[198,6613,429],{}," 嵌在父功能的 ",[198,6616,429],{}," 里，父 Reducer 带上子 Reducer。导航栈也放进 ",[198,6619,429],{},[3277,6621,126],{"href":127}," 里那份 ",[198,6624,3935],{}," 一样：栈是数据，离开页面是一次状态变化。",[179,6627,6628,6629,6631,6632,6634],{},"我用它之前先看三件事能不能付得起：每个点击都有对应的 ",[198,6630,5026],{},"，副作用能在测试里换成假的，这一屏值得单独一个 Feature。一个开关用 ",[198,6633,256],{}," 就够。",[207,6636,6637],{"id":6637},"我先按这个选",[1917,6639,6640,6647,6658,6668,6674,6682,6688,6691,6694],{},[1920,6641,6642,6643,1925,6645,335],{},"值只活在一块 SwiftUI 视图里：",[198,6644,256],{},[198,6646,533],{},[1920,6648,6649,6650,6652,6653,6655,6656,335],{},"这一屏有动作和多字段：",[198,6651,200],{},"，创建处用 ",[198,6654,256],{},"。很多页共用：",[198,6657,1953],{},[1920,6659,1965,6660,1933,6662,1971,6664,1974,6666,335],{},[198,6661,204],{},[198,6663,264],{},[198,6665,280],{},[198,6667,295],{},[1920,6669,6670,6671,6673],{},"UIKit：模型放控制器，只在 ",[198,6672,2822],{}," 里写控件。子视图用闭包把新值交上来。列表交一份 id 快照。",[1920,6675,6676,6677,1300,6679,6681],{},"这一屏的逻辑要离开控制器：ViewModel。UIKit 用 ",[198,6678,308],{},[198,6680,3611],{},"。SwiftUI 直接读。",[1920,6683,6684,6685,6687],{},"状态转移要能单测：收成 ",[198,6686,3839],{},"。请求做完，再投递下一个动作。",[1920,6689,6690],{},"工程已经是 Rx：绑定继续用 RxSwift。每屏还要把副作用和纯计算分开，再加 ReactorKit。",[1920,6692,6693],{},"新的 SwiftUI 模块、副作用多、要测整条路径：用 TCA。",[1920,6695,1957,6696,6698,6699,6701],{},[198,6697,1736],{},"。键盘和进后台用 ",[198,6700,3227],{},"。这两处都写进页面模型之后，再刷新界面。",[207,6703,6704],{"id":6704},"参考资料",[1917,6706,6707,6715,6726,6738,6745,6752,6759],{},[1920,6708,6709,6714],{},[3277,6710,429],{"href":6711,"rel":6712},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fswiftui\u002Fstate",[6713],"nofollow","：视图自己持有的存储",[1920,6716,6717,6721,6722,1300,6724],{},[3277,6718,3972],{"href":6719,"rel":6720},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fcombine",[6713],"：",[198,6723,308],{},[198,6725,3611],{},[1920,6727,6728,6721,6733,6735,6736],{},[3277,6729,6732],{"href":6730,"rel":6731},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fswiftui\u002Fmigrating-from-the-observable-object-protocol-to-the-observable-macro",[6713],"Migrating from the Observable Object protocol to the Observable macro",[198,6734,204],{}," 怎么迁到 ",[198,6737,200],{},[1920,6739,6740,6744],{},[3277,6741,3989],{"href":6742,"rel":6743},"https:\u002F\u002Fgithub.com\u002FReactiveX\u002FRxSwift",[6713],"：事件流和 UI 绑定",[1920,6746,6747,6751],{},[3277,6748,4007],{"href":6749,"rel":6750},"https:\u002F\u002Fgithub.com\u002FReactorKit\u002FReactorKit",[6713],"：Action、Mutation、State",[1920,6753,6754,6758],{},[3277,6755,4024],{"href":6756,"rel":6757},"https:\u002F\u002Fgithub.com\u002FReSwift\u002FReSwift",[6713],"：全局 Store 和 Reducer",[1920,6760,6761,6766],{},[3277,6762,6765],{"href":6763,"rel":6764},"https:\u002F\u002Fgithub.com\u002Fpointfreeco\u002Fswift-composable-architecture",[6713],"The Composable Architecture","：Feature、Effect、Store",[6768,6769,6770],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sjYur, html code.shiki .sjYur{--shiki-light:#6182B8;--shiki-light-font-style:italic;--shiki-default:#82AAFF;--shiki-default-font-style:italic;--shiki-dark:#82AAFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":221,"searchDepth":377,"depth":377,"links":6772},[6773,6781,6787,6793,6800,6801],{"id":209,"depth":377,"text":210,"children":6774},[6775,6776,6777,6778,6779,6780],{"id":329,"depth":384,"text":256},{"id":532,"depth":384,"text":533},{"id":751,"depth":384,"text":200},{"id":1088,"depth":384,"text":275},{"id":1263,"depth":384,"text":290},{"id":1499,"depth":384,"text":264},{"id":1979,"depth":377,"text":1980,"children":6782},[6783,6784,6785,6786],{"id":2003,"depth":384,"text":2003},{"id":2428,"depth":384,"text":2428},{"id":2908,"depth":384,"text":2908},{"id":3166,"depth":384,"text":3167},{"id":3282,"depth":377,"text":3282,"children":6788},[6789,6790,6791,6792],{"id":3384,"depth":384,"text":3326},{"id":3586,"depth":384,"text":3340},{"id":3368,"depth":384,"text":3368},{"id":3865,"depth":384,"text":3354},{"id":3942,"depth":377,"text":3942,"children":6794},[6795,6796,6797,6798,6799],{"id":4050,"depth":384,"text":3972},{"id":4771,"depth":384,"text":3989},{"id":5020,"depth":384,"text":4007},{"id":5614,"depth":384,"text":4024},{"id":5884,"depth":384,"text":4038},{"id":6637,"depth":377,"text":6637},{"id":6704,"depth":377,"text":6704},"SwiftUI 按所有权从 @State 排到 environment。UIKit 改完模型，再在一处写进控件。页面变大之后用 MVVM 或单向数据流。Combine、RxSwift、ReactorKit、TCA 负责把变化送出去。","md",[6805,6809,6810,6811,6812],{"label":6806,"icon":6807,"to":6711,"target":6808},"SwiftUI State","i-lucide-book-open","_blank",{"label":811,"icon":6807,"to":6730,"target":6808},{"label":3972,"icon":6807,"to":6719,"target":6808},{"label":4038,"icon":6807,"to":6763,"target":6808},{"label":4007,"icon":6807,"to":6749,"target":6808},{"date":6814,"categories":6815,"tags":6817},"2026-09-30",[6816],"ios",[108,6818,4030,130,3340],"SwiftUI",{"icon":91},{"title":6821,"description":6822},"iOS 状态管理：SwiftUI、UIKit 与架构","SwiftUI 状态从 @State、@Binding 排到 @Observable 和 environment。UIKit 用 apply 把模型写进控件。再记 MVP、MVVM、单向数据流，以及 Combine、RxSwift、ReactorKit 和 TCA。","ZbZK6mFgOLONQv3jAcNEYBqszzvtA8Fnjo9FSl8QYpg",[6825,6827],{"title":126,"path":127,"stem":128,"description":6826,"icon":48},"UIKit 用导航控制器上的页面栈管跳转，SwiftUI 用 NavigationStack 的 path 管跳转。两边都收成一个 Router，目的地用枚举。",{"title":134,"path":135,"stem":136,"description":6828,"icon":137},"传递和响应分开记。传递用图看 hitTest 怎么从窗口找到一个视图，响应用图看消息怎么沿 next 向上。",1790767047444]