<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[S.T.H Brewery 鍵盤藍綠藻]]></title><description><![CDATA[Swift, tech, and chit-chat]]></description><link>https://huangshihting.works/blog/</link><image><url>https://huangshihting.works/blog/favicon.png</url><title>S.T.H Brewery 鍵盤藍綠藻</title><link>https://huangshihting.works/blog/</link></image><generator>Ghost 4.29</generator><lastBuildDate>Tue, 14 Apr 2026 14:22:21 GMT</lastBuildDate><atom:link href="https://huangshihting.works/blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[How not to get desperate with MVVM implementation]]></title><description><![CDATA[<h3></h3><p>Let&#x2019;s imagine you have a small project, where you used to deliver new features in just 2 days. Then your project grows bigger. The delivery date becomes uncontrollable, from 2 days to 1 week, then 2 weeks. It drives you crazy! You keep complaining: a good product shouldn&</p>]]></description><link>https://huangshihting.works/blog/how-not-to-get-desperate-with-mvvm-implementation/</link><guid isPermaLink="false">61b96bec56cf0e0001441733</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 15 Dec 2021 04:15:54 GMT</pubDate><content:encoded><![CDATA[<h3></h3><p>Let&#x2019;s imagine you have a small project, where you used to deliver new features in just 2 days. Then your project grows bigger. The delivery date becomes uncontrollable, from 2 days to 1 week, then 2 weeks. It drives you crazy! You keep complaining: a good product shouldn&#x2019;t be so complicated! That&#x2019;s exactly what I have faced and it was really a bad time for me. Now, after working for a few years in this area, cooperating with many excellent engineers, I realized that the product design doesn&#x2019;t really make the code so complex. It&#x2019;s me who makes it so complicated.</p><p>We could have the experience writing spaghetti code which significantly hurts the performance of our projects.The question is how can we fix it? A good architecture pattern might help. In this article, we are going to talk about one of the good architecture: Model-View-ViewModel (MVVM). MVVM is a trending iOS architecture that focuses on the separation of development of user interface from development of the business logic.</p><p>The term &#x201C;good architecture&#x201D; may sound way too abstract. It&#x2019;s also difficult to know where to start. Here&#x2019;s a tip: Instead of focusing on the definition of the architecture, we can focus on how to <strong>improve the testability of the code</strong>. There&#x2019;re so many software architectures, such as MVC, MVP, MVVM, VIPER, It&#x2019;s clear, we might not be able to master all of those architectures. However, we are still able to keep a simple rule in mind: no matter what architecture we decide to use, the ultimate goal is to make test simpler. Using this approach we start thinking before writing code. We put emphasis on how to separate responsibility intuitively. Moreover, the design of the architecture seems clear and reasonable with this mindset, we won&#x2019;t stuck in trivial details anymore</p><h4 id="tldr">TL;DR</h4><p>In this article, you will learn:</p><ul><li>The reason we choose the MVVM over the Apple MVC</li><li>How to adapt MVVM to design a clearer architecture</li><li>How to write a simple real-world app based on the MVVM</li></ul><p>You won&#x2019;t see:</p><ul><li>The comparison between MVVM, VIPER, Clean, etc</li><li>A silver bullet that will solve all problems</li></ul><p>All of those architectures have the pros and the cons, but they are all designed to make the code simpler and clearer. So we decided to focus on <strong>why</strong> we select MVVM over MVC and <strong>how </strong>we move from MVC to MVVM. If you are interested in the cons of MVVM, please refer to the discussion at the end of this article.</p><p>So let&#x2019;s start!</p><h4 id="apple-mvc">Apple MVC</h4><p>MVC (Model-View-Controller) is Apple&#x2019;s recommended architectural pattern. The definition could be found here. The interaction between objects in the MVC is depicted as the following figure:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*la8KCs0AKSzVGShoLQo2oQ.png" class="kg-image" alt loading="lazy"></figure><p>In iOS/MacOS development, due to the introduction of the ViewController, it usually becomes:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*8XM4gfWIvaOl8kHiNlxLeg.png" class="kg-image" alt loading="lazy"></figure><p>The ViewController contains the View and owns the Model. The problem is we used to write the controller code as well as the view code in the ViewController. It makes the ViewController too complex. That&#x2019;s why we called it a Massive View Controller. While writing a test for the ViewController, you need to mock the view and the life cycle of it. But views are difficult to be mocked. And we actually don&#x2019;t want to mock the view if we only want to test the controller logic. All these things make writing tests so complicated.</p><p>So the MVVM is here to rescue.</p><h4 id="mvvm-%E2%80%94-model-%E2%80%94-view-%E2%80%94-viewmodel">MVVM&#x200A;&#x2014;&#x200A;Model&#x200A;&#x2014;&#x200A;View&#x200A;&#x2014;&#x200A;ViewModel</h4><p>MVVM is proposed by <a href="https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/" rel="noopener">John Gossman</a> in 2005. The main purpose of the MVVM is to move the data state from the View to the ViewModel. The data flow in MVVM could be drawn as the following figure:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*8MiNUZRqM1XDtjtifxTSqA.png" class="kg-image" alt loading="lazy"></figure><p>According to the definition, the View consists of only visual elements. In the View, we only do things like layout, animation, initializing UI components, etc. There&#x2019;s a special layer between the View and the Model called the ViewModel. The ViewModel is a canonical representation of the View. That is, the ViewModel provides a set of interfaces, each of which represents a UI component in the View. We use a technique called &#x201C;binding&#x201D; to connect UI components to ViewModel interfaces. So, in MVVM, we don&#x2019;t touch the View directly, we deal with business logic in the ViewModel and thus the View changes itself accordingly. We write presentational things such as converting Date to String in the ViewModel instead of the View. Therefore, it becomes possible to write a simpler test for the presentational logic without knowing the implementation of the View.</p><p>Let&#x2019;s go back and take a higher look at the figure above. In general, the ViewModel receives the user interaction from the View, fetches data from the Model, then process the data to a set of ready-to-display properties. The View updates itself after observing the change of the ViewModel. That&#x2019;s the whole story of the MVVM.</p><p>Specifically, for MVVM in iOS development, the UIView/UIViewController represent the View. We only do:</p><ol><li>Initiate/Layout/Present UI components.</li><li>Bind UI components with the ViewModel.</li></ol><p>On the other hand, in the ViewModel, we do:</p><ol><li>Write controller logics such as pagination, error handling, etc.</li><li>Write presentational logic, provide interfaces to the View.</li></ol><p>You might notice that the ViewModel is kinda complex. In the end of this article, we will discuss the bad part of the MVVM. Anyway, for a medium sized project, the MVVM is still a good choice to eat an elephant one bite at a time!</p><p>In the following sections, we are going to write a simple app with MVC pattern and then describe how to refactor the app to the MVVM pattern. The sample project with unit tests could be found on my GitHub:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/koromiko/Tutorial/tree/master/MVVMPlayground"><div class="kg-bookmark-content"><div class="kg-bookmark-title">koromiko/Tutorial</div><div class="kg-bookmark-description">Tutorial - Code for https://koromiko1104.wordpress.com</div><div class="kg-bookmark-metadata"><span class="kg-bookmark-author">github.com</span></div></div><div class="kg-bookmark-thumbnail"><img src="&quot;https://cdn-images-1.medium.com/fit/c/320/320/0*52ieDfGLSx2bxUhD.&quot;" alt></div></a></figure><p>Let&#x2019;s start!</p><h3 id="a-simple-gallery-app-%E2%80%94-mvc">A simple gallery app&#x200A;&#x2014;&#x200A;MVC</h3><p>We are going to write a simple app, in which:</p><ol><li>The app fetches popular photos from 500px API and lists photos in a UITableView.</li><li>Each cell in the table view shows a title, a description and the created date of a photo.</li><li>Users are not allowed to click photos which are not labeled for_sale.</li></ol><p>In this app, we have a struct named <strong>Photo</strong>, it represents a single photo. Here&#x2019;s the interface of the <strong>Photo</strong> class:</p><p>The initial view controller of the app is a UIViewController containing a table view called <strong>PhotoListViewController</strong>. We fetch <strong>Photo</strong> objects through the <strong>APIService</strong> in the <strong>PhotoListViewController</strong>, and reload the table view after photos are fetched:</p><p>The <strong>PhotoListViewController</strong> is also a datasource of the table view:</p><p>In the <strong>func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell</strong>, we select the corresponding <strong>Photo</strong> object and assign the title, description, and the date to a cell. Since the <strong>Photo</strong>.date is a Date object, we have to convert it to a String using a DateFormatter.</p><p>The following code is the implementation of the table view delegate:</p><p>We select the corresponding Photo object in <strong>func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -&gt; IndexPath?</strong>, check the <strong>for_sale</strong> property. If it&#x2019;s true, save the <strong>selectedIndexPath</strong> for a segue. If not, display an error message and return nil to prevent segueing.</p><p>The source code of <strong>PhotoListViewController</strong> could be found <a href="https://github.com/koromiko/Tutorial/blob/MVC/MVVMPlayground/MVVMPlayground/Module/PhotoList/PhotoListViewController.swift" rel="noopener">here</a>, please refer to the tag &#x201C;MVC&#x201D;.</p><p>So what&#x2019;s wrong with the code above? In the <strong>PhotoListViewController</strong>, we can find the presentational logic such as converting Date to String and when to start/stop the activity indicator. We also have the View code such as the implementation of showing/hiding the table view. In addition, there&#x2019;s another dependency, the API service, in the view controller. If you plan to write tests for the <strong>PhotoListViewController</strong>, you will find that you&#x2019;re stuck since it&#x2019;s too complicated. We have to mock the <strong>APIService</strong>, mock the table view and mock the cell to test the whole <strong>PhotoListViewController</strong>. Phew!</p><p>Remember that we want to make writing tests easier? Let&#x2019;s try MVVM approach!</p><h4 id="try-mvvm">Try MVVM</h4><p>In order to solve the problem, our first priority is to clean up the view controller, split the view controller into two parts: the View and the ViewModel. To be specific, we are going to:</p><ol><li>Design a set of interfaces for binding.</li><li>Move the presentational logic and controller logic to the ViewModel.</li></ol><p>First thing first, let&#x2019;s take a look at the UI components in the View:</p><ol><li>activity Indicator (loading/finish)</li><li>tableView (show/hide)</li><li>cells (title, description, created date)</li></ol><p>So we can abstract the UI components to a set of canonical representations:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*ktmfaTJajU0NYrCBq8iqnA.png" class="kg-image" alt loading="lazy"></figure><p>Each UI component has a corresponding property in the ViewModel. We can say that what we will see in the View should be the same as what we see in the ViewModel.</p><p>But how do we do the binding?</p><h4 id="implement-the-binding-with-closure">Implement the Binding with Closure</h4><p>In Swift, there are various ways to achieve the &#x201C;binding&#x201D;:</p><ol><li>Use KVO (Key-Value Observing) pattern.</li><li>Use 3rd party libraries for FRP (Functional Reactive Programming) such as RxSwift and ReactiveCocoa.</li><li>Craft it yourself.</li></ol><p>Using the KVO pattern isn&#x2019;t a bad idea, but it might create a huge delegate method and we have to be careful about the addObserver/removeObserver, which might be a burden to the View. The ideal way for binding is to use the binding solution in FRP. If you are familiar with functional reactive programming then go for it! If not, I wouldn&#x2019;t recommend using FRP just for binding because it&#x2019;s kind of confusing to crack a nut using a sledgehammer. <a href="http://five.agency/solving-the-binding-problem-with-swift/" rel="noopener">Here</a> is a brilliant article talking about using the decorator pattern to craft the binding yourself. In this article, we are going to put things simpler. We bind things using a closure. Practically, in the ViewModel an interface/property for binding looks like this:</p><p>On the other hand, in the View, we assign a closure to the propChanged as a callback closure for value updates.</p><p>Every time the property prop is updated, the propChanged is called. So we are able to update the View according to the change of the ViewModel. Quite straightforward, right?</p><h4 id="interfaces-for-binding-in-viewmodel">Interfaces for binding in ViewModel</h4><p>Now, let&#x2019;s start to design our ViewModel, the <strong>PhotoListViewModel</strong>. Given the following three UI components:</p><ol><li>tableView</li><li>cells</li><li>activity indicator</li></ol><p>We create the interfaces/properties for binding in the <strong>PhotoListViewModel</strong>:</p><p>Each <strong>PhotoListCellViewModel</strong> object forms a canonical representation of a cell in the table view. It provides data interfaces for rendering a UITableView cell. We put all <strong>PhotoListCellViewModel</strong> objects into an array <strong>cellViewModels</strong>, the number of cells is exactly the number of items in that array. We can say that the array, <strong>cellViewModels</strong>, represents the table view. Once we update the <strong>cellViewModels</strong> in ViewModel, the closure <strong>reloadTableViewClosure</strong> will be called and the View updates correspondingly.</p><p>A single <strong>PhotoListCellViewModel</strong> looks like this:</p><p>As you can see, the properties of the <strong>PhotoListCellViewModel</strong> provide interface for binding to UI components in the View.</p><h4 id="bind-the-view-with-the-viewmodel">Bind the View with the ViewModel</h4><p>With the interfaces for binding, now we&#x2019;ll focus on the View part. First, in the <strong>PhotoListViewController</strong>, we initialize callback closures in viewDidLoad:</p><p>Then we are going to refactor the datasource. In MVC pattern, we setup presentational logics in the <strong>func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell</strong>, now we have to move the presentation logic to the ViewModel. The refactored datasource looks like:</p><p>The data flow now becomes:</p><ol><li>The PhotoListViewModel starts to fetch data.</li><li>After the data fetched, we create <strong>PhotoListCellViewModel</strong> objects and update the <strong>cellViewModels.</strong></li><li>The <strong>PhotoListViewController</strong> is notified of the update and then layouts cells using the updated <strong>cellViewModels.</strong></li></ol><p>It could be depicted as the following figure:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*w4bDvU7IlxOpQZNw49fmyQ.png" class="kg-image" alt loading="lazy"></figure><h4 id="dealing-with-user-interaction">Dealing with user interaction</h4><p>Let&#x2019;s move on to the user interaction. In the <strong>PhotoListViewModel</strong>, we create a function:</p><p>When the user clicks on a single cell, the <strong>PhotoListViewController</strong> notifies the <strong>PhotoListViewModel</strong> using this function. So we can refactor the delegate method in <strong>PhotoListViewController</strong>:</p><p>It means that once the <strong>func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -&gt; IndexPath?</strong> was called due to a user interaction, the action is passed to the <strong>PhotoListViewModel</strong>. The delegate function decides whether to segue or not based on the property isAllowSegue provided by the <strong>PhotoListViewModel</strong>. We successfully remove the state from the View. &#x1F37B;</p><h4 id="the-implementation-of-the-photolistviewmodel">The Implementation of the PhotoListViewModel</h4><p>It&#x2019;s a long journey, right? Bear with me, we are touching the core of the MVVM! In the <strong>PhotoListViewModel</strong>, we have an array named <strong>cellViewModels</strong>, which represents the table view in the View.</p><p>How do we fetch data and get the array ready? We actually do two things in the initialization of the ViewModel:</p><p>1. Inject the dependency: the <strong>APIService</strong><br>2. Fetch data using the <strong>APIService</strong></p><p>In the code snippet above, we set the property isLoading to true before starting to fetch the data from the <strong>APIService</strong>. Thanks to the binding we did before, set the isLoading to true means that the View will switch the active indicator on. In the callback closure of the <strong>APIService</strong>, we process the fetched photo models and set the isLoading to false. We don&#x2019;t need to touch the UI component directly, but it&#x2019;s clear that the UI components work as what we expected when we changed those properties of the ViewModel.</p><p>Then here&#x2019;s the implementation of the <strong>processFetchedPhoto( photos: [Photo] )</strong> :</p><p>It does a simple job, wrapping the photo models into an array of <strong>PhotoListCellViewModel</strong>. When the property, <strong>cellViewModels</strong>, is updated the table view in the View reloads correspondingly.</p><p>Yay, we crafted the MVVM &#x1F389;</p><p>The sample app could be found on my GitHub:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/koromiko/Tutorial/tree/MVC/MVVMPlayground"><div class="kg-bookmark-content"><div class="kg-bookmark-title">koromiko/Tutorial</div><div class="kg-bookmark-description">Tutorial - Code for https://koromiko1104.wordpress.com</div><div class="kg-bookmark-metadata"><span class="kg-bookmark-author">github.com</span></div></div><div class="kg-bookmark-thumbnail"><img src="&quot;https://cdn-images-1.medium.com/fit/c/320/320/0*fQg1BgIxhtPpezvd.&quot;" alt></div></a></figure><p>You might want to try the MVC version (tag: MVC) and then the MVVM one (the latest commit)</p><h4 id="recap">Recap</h4><p>In this article, we successfully converted a simple app from the MVC pattern to the MVVM pattern. And we:</p><ul><li>Made a binding theme using the closure.</li><li>Removed all controller logic from the View.</li><li>Created a testable ViewModel.</li></ul><h4 id="discussion">Discussion</h4><p>As I mentioned above, architectures all have the pros and the cons. After reading my article, you must have some ideas about what&#x2019;s the cons of the MVVM. There are good articles talking about the bad parts of the MVVM, such as:</p><p><a href="http://khanlou.com/2015/12/mvvm-is-not-very-good/" rel="noopener">MVVM is Not Very Good&#x200A;&#x2014;&#x200A;Soroush Khanlou</a><br><a href="http://www.danielhall.io/the-problems-with-mvvm-on-ios" rel="noopener">The Problems with MVVM on iOS&#x200A;&#x2014;&#x200A;Daniel Hall</a></p><p>My biggest concern about MVVM is the ViewModel does too many things. As I mentioned in this article, we have the controller and the presenter in the ViewModel. Also, two roles, the builder and the router, are not included in the MVVM pattern. We used to put the builder and the router in the ViewController. If you&#x2019;re interested in a clearer solution, you might want to check the MVVM+FlowController (<a href="http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/" rel="noopener">Improve your iOS Architecture with FlowControllers</a>) and two well-known architecture, <a href="https://www.objc.io/issues/13-architecture/viper/" rel="noopener">VIPER</a> and <a href="https://hackernoon.com/introducing-clean-swift-architecture-vip-770a639ad7bf" rel="noopener">Clean by Uncle Bob</a>.</p><h4 id="start-small">Start small</h4><p>There&#x2019;s always a better solution. As professional engineers, we&#x2019;re always learning how to improve the code quality. Developers like me were used to be overwhelmed by so many architectures and don&#x2019;t know how to start writing unit tests. So the MVVM is a good place to begin your journey. It&#x2019;s simple and the testability is still good. In another Soroush Khanlou&#x2019;s article, <a href="http://khanlou.com/2014/09/8-patterns-to-help-you-destroy-massive-view-controller/" rel="noopener">8 Patterns to Help You Destroy Massive View Controller</a>, there are many good patterns and some of them are also adopted by the MVVM. Instead of being hampered by a gigantic architecture, how about we start writing test with small yet powerful MVVM pattern?</p><blockquote>&#x201C;The secret to getting ahead is getting started.&#x201D;&#x200A;&#x2014;&#x200A;Mark Twain</blockquote><p>In the next article, I will continue to talk about writing unit tests for our simple gallery app. Stay tuned!</p><p>If you have any questions please don&#x2019;t hesitate to leave a comment. Any kind of discussion is also welcome! Thank you for your attention.</p><h4 id="references">References</h4><p><a href="https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/" rel="noopener">Introduction to Model/View/ViewModel pattern for building WPF apps&#x200A;&#x2014;&#x200A;John Gossman</a><br><a href="https://www.objc.io/issues/13-architecture/mvvm/" rel="noopener">Introduction to MVVM&#x200A;&#x2014;&#x200A;objc</a><br><a href="https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52">iOS Architecture Patterns&#x200A;&#x2014;&#x200A;Bohdan Orlov</a><br><a href="http://swiftyjimmy.com/category/model-view-viewmodel/" rel="noopener">Model-View-ViewModel with swift&#x200A;&#x2014;&#x200A;SwiftyJimmy</a><br><a href="https://www.toptal.com/ios/swift-tutorial-introduction-to-mvvm" rel="noopener">Swift Tutorial: An Introduction to the MVVM Design Pattern&#x200A;&#x2014;&#x200A;DINO BARTO&#x160;AK</a><br><a href="https://msdn.microsoft.com/en-us/magazine/dn463790.aspx" rel="noopener">MVVM&#x200A;&#x2014;&#x200A;Writing a Testable Presentation Layer with MVVM&#x200A;&#x2014;&#x200A;Brent Edwards</a><br><a href="http://rasic.info/bindings-generics-swift-and-mvvm/" rel="noopener">Bindings, Generics, Swift and MVVM&#x200A;&#x2014;&#x200A;Srdan Rasic</a></p>]]></content:encoded></item><item><title><![CDATA[Applying Unit Tests to MVVM with Swift]]></title><description><![CDATA[<h3></h3><p>In my previous article, <a href="https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b">How not to get desperate with MVVM implementation</a>, we learned the Model-View-ViewModel (MVVM) architecture and saw how to create a simple gallery app by using it. With the help of the MVVM, we separate the business logic and presentational logic from the view logic. The separation</p>]]></description><link>https://huangshihting.works/blog/untitled/</link><guid isPermaLink="false">61b96ba356cf0e000144172c</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 15 Dec 2021 04:14:40 GMT</pubDate><content:encoded><![CDATA[<h3></h3><p>In my previous article, <a href="https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b">How not to get desperate with MVVM implementation</a>, we learned the Model-View-ViewModel (MVVM) architecture and saw how to create a simple gallery app by using it. With the help of the MVVM, we separate the business logic and presentational logic from the view logic. The separation of concerns (SoC) makes writing unit tests easier than ever. Even though the idea of the MVVM is simple, writing unit tests for its various use cases is still worth mentioning. So in this article, we will step further and learn how to brew unit tests for MVVM.</p><p>In short, I will cover two techniques:</p><ul><li>How to design a mock to simulate different network states.</li><li>How to use stubs to test user interaction.</li></ul><p>Let&#x2019;s start with the gallery app we crafted in my previous <a href="https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b">guide</a>, mentioned above.</p><h3 id="a-simple-gallery-app">A simple gallery app</h3><p>To recall, the simple gallery app has the following functionalities:</p><ol><li>The app fetches popular photos from 500px API and lists photos in a UITableView.</li><li>Each cell in the table view shows a title, a description and the created date of a photo.</li></ol><p>The first screen in the app is called <strong>PhotoListViewController</strong>, the data flow is depicted in this figure:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*w4bDvU7IlxOpQZNw49fmyQ.png" class="kg-image" alt loading="lazy"></figure><p>The APIService takes the responsibility of the network layer, such as setting up the URL, sending the request, etc. The <strong>PhotoListViewModel</strong> asks the <strong>APIService</strong> for photo objects and provides the presentational interfaces for the <strong>PhotoListViewController</strong>. The <strong>PhotoListViewController</strong> is a simple View, rendering the visible element according to the data presented by the <strong>PhotoListViewModel</strong>.</p><p>Here are some use cases that we are going to test in this article:</p><ol><li>The <strong>PhotoListViewModel</strong> should fetch data from the <strong>APIService</strong>.</li><li>The <strong>PhotoListViewModel</strong> should display an error message if the request failed.</li></ol><p>The <strong>PhotoListViewModel</strong> should allow the segue to the detail page if a user presses on a &#x201C;for sale&#x201D; photo.</p><h3 id="mvvm-and-dependency-injection">MVVM and Dependency Injection</h3><p>Here&#x2019;s part of the implementation of the <strong>PhotoListViewModel</strong>:</p><p>We use a technique named dependency injection (DI) to design our <strong>PhotoListViewModel</strong>. The property, <em>apiService</em>, is a dependency of the <strong>PhotoListViewModal</strong>. The apiService takes care of all networking things and provide a handful interface for the <strong>PhotoListViewModel</strong> to get returned data. The most important thing is, it can be assigned by all objects conforming the <strong>APIServiceProtocol</strong>.</p><p>In the production environment we assign an <strong>APIService</strong> object, which connects to a real server, to the <strong>PhotoListViewModel</strong>. On the other hand, in the test environment, we inject a mock <strong>APIService</strong> object instead. The mock APIService (<strong>MockAPIService</strong>) object doesn&#x2019;t connect to the real server, it&#x2019;s an object designed only for the test. Both <strong>APIService</strong> and <strong>MockAPIService</strong> conform the protocol, <strong>APIServiceProtocol</strong>, so that we are able to inject different dependency in different situation.</p><p>If you&#x2019;re not familiar with the idea of dependency injection, feel free to check my article: <a href="https://medium.com/flawless-app-stories/the-complete-guide-to-network-unit-testing-in-swift-db8b3ee2c327">The complete guide to Network Unit Testing in Swift</a> :)</p><p>The following figure shows the relationship between the <strong>PhotoListViewModel</strong> and its dependency:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/1*XbNX4vkU-GfjthvbgN9_Tg.png" class="kg-image" alt loading="lazy"></figure><p>The idea of injection will be more clear in the <em>setUp()</em> and the <em>tearDown()</em> code snippets below. We initialize the <strong>PhotoListViewModel </strong>with an <strong>APIService</strong> object, but we use a <strong>MockAPIService</strong> object instead in the test environment:</p><p>In the following section, we are going to see how to design the <strong>MockAPIService</strong> to simulate a different situations for all test cases.</p><h3 id="behavior-test">Behavior test</h3><p>Our first use case is:</p><ol><li>The <strong>PhotoListViewModel</strong> should fetch data from the <strong>APIService</strong>.</li></ol><p>That is, we want to check if the <strong>PhotoListViewModel</strong> really requests data from the <strong>APIService</strong>.</p><p>Here&#x2019;s the implementation of the mock:</p><p>In order to be injected into the <strong>PhotoListViewModel</strong>, the mock should conform the <strong>APIServiceProtocol</strong> protocol. So we create the required method, <em>fetchPopularPhoto(complete:)</em> to conform the protocol. Now we want to make sure if the <strong>PhotoListViewModel</strong> called the <em>fetchPopularPhoto(complete:)</em> method to fetch the data, so we create a property, <em>isFetchPopularPhotoCalled</em>, in the <strong>MockAPIService</strong>. Remember that the <em>apiService</em> property is a <strong>MockAPIService</strong>object in a test environment? The <em>isFetchPopularPhotoCalled</em> will be set to true if the mock function <em>fetchPopularPhoto(complete:)</em> is called by the <strong>PhotoListViewModel</strong>.</p><p>Here&#x2019;s is the code for our first test:</p><p>This is a simple test case. The <em>sut</em> (SUT, System Under Test) is a <strong>PhotoListViewModel </strong>instance. The code snippet shows that when the <strong>PhotoListViewModel</strong> fetches data, we check if it called the <em>fetchPopularPhoto(complete:)</em> method. By using this technique, we are able to check if <strong>PhotoListViewModel</strong> calls specified methods for the dependency injection. In other words, we successfully test the behavior of our ViewModel.</p><h3 id="success-or-failure">Success or Failure?</h3><p>In addition to the behavior test, we also want to see if the <strong>PhotoListViewModel</strong> correctly handles networking states. By using the DI technique, we are able to simulate the success and failure networking states by changing the response of the <strong>MockAPIService</strong>. In this section, we are going to see how to change the response state of the <strong>MockAPIService</strong>. Let&#x2019;s check the second use case as in the example below:</p><ol><li>The <strong>PhotoListViewModel</strong> should display an error message if the request failed.</li></ol><p>According to the use case, we write the following test code:</p><p>The error object represents the given condition of this test case: the request will fail due to a permission issue. After setting up the given condition, we call <em>sut.initFetch()</em> to start fetching data. Then here&#x2019;s a trick: we ask the mock to fail the request by calling the <em>fetchFail(error:)</em> function. Finally, we assert the alert message of the <strong>PhotoListViewModel </strong>to see if it handles the error correctly.</p><p>So, how to design the MockAPIService? Let&#x2019;s go back to the implementation of the <strong>PhotoListViewModel</strong>.<em>initFetch()</em>&#xFF1A;</p><p>The <em>initFetch()</em> triggers the <em>apiService.fetchPopularPhoto(complete:)</em>, assigns a callback closure, and waits for the apiService to call the callback closure. The callback closure is the key!. If we want to simulate a failed request, we can trigger that closure with an error in the <strong>MockAPIService</strong>. This is the implementation of the <strong>MockAPIService</strong>:</p><p>In the code snippet, when the <em>fetchPopularPhoto(complete:)</em> is called, the callback closure is saved to the <em>completeClosure</em> for the later use. On the other hand, for the <strong>PhotoListViewModel</strong>, the function call is finished but the escaping closure is still pending. The closure won&#x2019;t be triggered until the <em>fetchSuccess()</em> or the <em>fetchFail(error:)</em> is called. Finally, when we call the <em>fetchSuccess()</em> or the <em>fetchFail(error:)</em>, the <strong>PhotoListViewModel</strong> receives the response data and continue to finish its jobs.</p><p>Now the <strong>MockAPIService</strong> is able to simulate any kind of asynchronous request. For example, we are able to assert the loading state of the ViewModel: isLoading should be true before <em>fetchSuccess()</em> is called. This is a powerful technique because it mimics an async call, and it could be done immediately.</p><h3 id="let%E2%80%99s-get-some-stubs-ready">Let&#x2019;s get some stubs ready</h3><p>The ViewModel receives the user interaction and changes the presentation with respect to the interaction. In MVVM, the user interactions are abstracted into a set of methods such as <em>userPressed()</em>, <em>userSwipe()</em>, etc. Therefore, testing the user interactions is straightforward: we call a certain method and assert the corresponding property of the ViewModel. Let&#x2019;s check the third use case we are going to test:</p><ol><li>The <strong>PhotoListViewModel</strong> should allow the segue to the detail page if a user presses on a &#x201C;for sale&#x201D; photo.</li></ol><p>As we did before, we first write the test code:</p><p>This test case describes a user who presses on the first cell, and we are going to see if the segue is allowed. The problem is, we haven&#x2019;t fetched any photo yet and the sut is currently in the empty state. So if we trigger the <em>sut.userPressed(at:)</em> at this stage, the out of bounds exception will be raised. We need to grab some data first!</p><p>The data we need is <strong>Photo</strong> objects. We want the <strong>MockAPIService</strong> to return the photo objects to the <strong>PhotoListViewModel</strong>, just like what it&#x2019;s supposed to be in the production environment. After the apiService returns the data to the <strong>PhotoListViewModel</strong>, we are able to trigger the <em>sut.userPressed(at:)</em> and assert the <em>isAllowSegue</em>. The photo objects we created for the test are called &#x201C;stubs&#x201D;. I won&#x2019;t dive deep into the definition of &#x201C;stub&#x201D;, but if you&#x2019;re interested, go check my <a href="https://medium.com/flawless-app-stories/cracking-the-tests-for-core-data-15ef893a3fee">article</a> on cracking tests for Core Data.</p><p>The implementation of the <strong>MockAPIService</strong> now becomes:</p><p>We create an array, <em>completePhotos</em>, to save the stubs. And those stubs will be returns to the <strong>PhotoListViewModel</strong> once the <strong>PhotoListViewModel</strong> calls <em>fetchPopularPhoto(complete:)</em>. Then let&#x2019;s take a look at our test code:</p><p>The <strong>StubGenerator</strong>().<em>stubPhotos()</em> generates couple of photo objects. Then we assign the photo objects to the mockAPIService.completePhotos. When the request finished (<em>mockAPIService.fetchSuccess()</em> is called), the <strong>PhotoListViewModel</strong> will receive those photo stubs via the callback closure. With the help of those stubs, we are able to assert a certain action such as user presses on a specific IndexPath and so on.</p><h3 id="more-tests%E2%80%A6">More tests&#x2026;</h3><p>We are able to use the knowledge we have learned to write more test cases:</p><ol><li>The loading animation should start when the network request starts.</li><li>The loading animation should stop when the request completes.</li><li>The table view should render correctly.</li><li>It should display error message when user press on the photo that is not for sale.</li></ol><p>All of those are included in my GitHub:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/koromiko/Tutorial/tree/master/MVVMPlayground"><div class="kg-bookmark-content"><div class="kg-bookmark-title">koromiko/Tutorial</div><div class="kg-bookmark-description">Tutorial - Code for https://koromiko1104.wordpress.com</div><div class="kg-bookmark-metadata"><span class="kg-bookmark-author">github.com</span></div></div><div class="kg-bookmark-thumbnail"><img src="&quot;https://cdn-images-1.medium.com/fit/c/320/320/0*IS8qI-nTjYw2z4k9.&quot;" alt></div></a></figure><h3 id="recap">Recap</h3><p>In this article we used a <strong>MockAPIService</strong> to check:</p><ul><li>If the SUT correctly interacts with the APIService.</li><li>If the SUT handles the error state correctly.</li><li>If the SUT handles the user interaction correctly.</li></ul><h3 id="conclusion">Conclusion</h3><p>There are still a lot of things to do. As I mentioned in my previous article, the MVVM has its limitations and cons, such as the anti-pattern of the single-responsibility and the lack of the builder and the router. When it comes to writing tests, I didn&#x2019;t cover the unit test for the binding and routing. Besides, the state of the SUT could be handled better (There&#x2019;s a good article talking about the state: <a href="https://jobandtalent.engineering/ios-architecture-an-state-container-based-approach-4f1a9b00b82e" rel="noopener">iOS Architecture: A State Container based approach</a>).</p><p>However, the MVVM does remove the obstacle in writing tests: it&#x2019;s easier to test the presentational logic. And writing tests for user interaction becomes straightforward as well. The most important thing is that the MVVM is simple and intuitive. It&#x2019;s a good start point for you to understand the idea of dependency injection and module composition. In the following articles, I&#x2019;ll continuously refactor our simple gallery app to make it more consolidated and beautiful. So, stay tuned!</p><h3 id="reference">Reference</h3><p><a href="https://clean-swift.com/testing-view-controller-part-1/" rel="noopener">Testing View Controller&#x200A;&#x2014;&#x200A;Part 1&#x200A;&#x2014;&#x200A;Clean Swift</a> <br><a href="http://marinbenc.com/unit-testing-ios-in-swiftpart-2-a-testable-architecture" rel="noopener">Unit Testing iOS in Swift&#x200A;&#x2014;&#x200A;Part 2: A Testable Architecture</a> <br><a href="http://swiftyjimmy.com/unit-test-mvvm-in-swift/" rel="noopener">How to unit test ViewModel in Swift&#x200A;&#x2014;&#x200A;SwiftyJimmy</a></p>]]></content:encoded></item><item><title><![CDATA[Build it, Test it, Deliver it!
Complete iOS Guide on Continuous Delivery with fastlane and Jenkins]]></title><description><![CDATA[<h3></h3><p>iOS/macOS development is really interesting. <br>You can get domain knowledge in so many fields! You might learn the graphic techniques such as Bezier or 3D transform. And you need to understand how to work with database or design an efficient schema. Moreover, you should be able to manage memory</p>]]></description><link>https://huangshihting.works/blog/build-it-test-it-deliver-it-complete-ios-guide-on-continuous-delivery-with-fastlane-and-jenkins/</link><guid isPermaLink="false">61b96acb56cf0e0001441723</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 15 Dec 2021 04:12:06 GMT</pubDate><content:encoded><![CDATA[<h3></h3><p>iOS/macOS development is really interesting. <br>You can get domain knowledge in so many fields! You might learn the graphic techniques such as Bezier or 3D transform. And you need to understand how to work with database or design an efficient schema. Moreover, you should be able to manage memory in an embedded-system way (especially for those who were in the great MRC era). All of those make iOS/macOS development so diverse and also challenging.</p><p><strong>In this article, we&#x2019;ll learn yet another thing you probably need to know: Continuous Delivery (CD)</strong>. Continuous Delivery is a software approach that helps you release products reliably, at any time. The CD usually comes with the term <strong>Continuous Integration (CI)</strong>. CI is also a software engineering technique. It means that the system continuously merges developers&#x2019; works to a mainline all the time. Both CI and CD are not only useful to a big team but also useful to a one-man team. And if you are a sole developer in a one-man team, CD probably means more to you since delivery is unavoidable to every application developer. So this article will focus on how to build a CD system for your application. Fortunately, all of those techniques can be adopted in the construction of a CI system as well.</p><p>Imagine that we are developing an iOS app named <strong>Brewer</strong>, then our workflow will look pretty simple:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*0jXhBlVFGke_tF_2." class="kg-image" alt loading="lazy"></figure><p>First, we develop. Then QA team helps us manually test the app. After the QA approves the test build, we release (submit to the AppStore for review) our app. In different stages, we have different environments. During development, we build the app in a staging environment for testing every day/night. When QA team is testing, we prepare an app built with production environment. This could be a weekly build specially for QA team. Finally, we submit the app using Production environment. Such final builds could have no predefined schedule at all.</p><p>Let&#x2019;s take a deeper look at the delivery part. You might find that we have a lot of duplicated work on building test apps. Here&#x2019;s what the CD system can help you with. Specifically, our CD system needs to:</p><ol><li>Build the app in different environments (staging/production).</li><li>Sign the code according to the environment we choose.</li><li>Export the app and send it to a distribution platform (such as Crashlytics and TestFlight).</li><li>Build the app according to a specific schedule.</li></ol><h3 id="outline"><strong>Outline</strong></h3><p>Here is what we&#x2019;re gonna do in this article:</p><ul><li><strong>Setup your project</strong>: How to setup your project to support the switch between different environments.</li><li><strong>Sign the code manually</strong>: How to handle the certificate and provisioning profile manually.</li><li><strong>Standalone environment</strong>: How to Use Bundler to isolate the system environment.</li><li><strong>Build with fastlane &#x1F680;</strong>: How to build and export the app using fastlane.</li><li><strong>Jenkins will be your server for tonight</strong>: How Jenkins helps you scheduling your tasks.</li></ul><p>Before we start, you probably want to check out:</p><ul><li>What&#x2019;s <a href="https://fastlane.tools/" rel="noopener">fastlane</a></li><li>What&#x2019;s <a href="https://jenkins.io/" rel="noopener">Jenkins</a></li><li>What&#x2019;s <a href="https://developer.apple.com/support/code-signing/" rel="noopener">Code signing</a></li></ul><p>If you&#x2019;re a busy guy/girl, no worries, I made the Brewer app a public repository with sample script for you!</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/koromiko/Brewer"><div class="kg-bookmark-content"><div class="kg-bookmark-title">koromiko/Brewer</div><div class="kg-bookmark-description">Brewer - We brew beer every night!</div><div class="kg-bookmark-metadata"><span class="kg-bookmark-author">github.com</span></div></div><div class="kg-bookmark-thumbnail"><img src="&quot;https://cdn-images-1.medium.com/fit/c/320/320/0*TfEp1ZF8FdIg-0PJ.&quot;" alt></div></a></figure><p>So, let&#x2019;s start!</p><h3 id="setup-your-project">Setup your project</h3><p>We usually connect to a development server or a staging server on developer test stage. We also need to connect to a production server when releasing the app to a QA team or AppStore. Switching the server by editing the code might be not a good idea. Here we use the build configuration and the compiler flag in Xcode. We won&#x2019;t dive into detail about the configuration. If you&#x2019;re interested in the setup, check this great article by <a href="https://twitter.com/D4Yuri" rel="noopener">Yuri Chukhlib</a>:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://medium.com/flawless-app-stories/manage-different-environments-in-your-swift-project-with-ease-659f7f3fb1a6"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Manage different environments in your Swift project with ease</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><span class="kg-bookmark-author">medium.com</span></div></div><div class="kg-bookmark-thumbnail"><img src="&quot;https://cdn-images-1.medium.com/fit/c/320/320/1*Rk8JulyapCiTCUtLsnsEcQ.png&quot;" alt></div></a></figure><p>In our Brewer project, we have three build configurations:</p><ul><li>Staging</li><li>Production</li><li>Release</li></ul><p>Each of which maps to a specific Bundle identifier:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*CyWbsYZ-6ZzrrY9y." class="kg-image" alt loading="lazy"></figure><p>We set up the flag to help our code know which server environment are we using.</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*k8Fb1CXd1SpIgFoK." class="kg-image" alt loading="lazy"></figure><p>So we can do something like this:</p><p>Now we are able to change the staging/production environment by changing the build configuration, without modifying any code! &#x1F389;</p><h3 id="sign-the-code-manually">Sign the code manually</h3><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*rfY9x3TB7VEnUENC." class="kg-image" alt loading="lazy"></figure><p>This is a well-known red button for every iOS/macOS developer. We start every project by unchecking this box. But why it&#x2019;s so notorious? You might know that it download the certificate and the provisioning profile, embed those to your project and system. If any file missed, it makes a new one for you. For a one-man team, nothing wrong here. But if you&#x2019;re in a big team, you might accidentally refresh the original certificate, and then the building system stops working due to the invalid certificate. To us, it&#x2019;s a black box hiding too much information.</p><p>So in our Brewer project, we want to do this by hand. We have three app IDs in our configuration:</p><ul><li><strong>works.sth.brewer.staging</strong></li><li><strong>works.sth.brewer.production</strong></li><li><strong>works.sth.brewer</strong></li></ul><p>We&#x2019;ll focus on first two configurations in this article. Now we need to prepare:</p><ul><li><strong>Certificate</strong>: An Ad Hoc/App Store distribution certificate, in .p12 format.</li><li><strong>Provisioning Profiles</strong>: Ad Hoc distribution provisioning profiles for two app identifiers, <strong>works.sth.brewer.staging</strong>and <strong>works.sth.brewer.production</strong>.</li></ul><p>Note that we need the p12 format of the certificate file, since we want it to be portable to different machines, and only .p12 format containing the private key to the certificate. Check <a href="https://stackoverflow.com/questions/39091048/convert-cer-to-p12" rel="noopener">this</a> to see how to convert .cer file (DEM format) to .p12 (P12 format) file.</p><p>Now we have our code signing files in a folder:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*qnhxIxQwwRlMeTP3." class="kg-image" alt loading="lazy"></figure><p>Those files are used by the CD system, so please put the folder on the CD machine. Please <strong>don&#x2019;t</strong> put those files with your project, <strong>don&#x2019;t</strong> commit them to your project repository. It&#x2019;s okay to host the code signing files in a different private repository. You might want to check the security discussion in <a href="https://docs.fastlane.tools/actions/match/#is-this-secure" rel="noopener">match&#x200A;&#x2014;&#x200A;fastlane docs</a>.</p><h3 id="build-with-fastlane-%F0%9F%9A%80">Build with fastlane &#x1F680;</h3><p><a href="https://docs.fastlane.tools/" rel="noopener">fastlane</a> is a tool that automates the development and release workflow. For example, it can build the app, run the unit test, and upload the binary to Crashlytics, in one script. You don&#x2019;t need to do those things step by step manually.</p><p>In this project, we are going to use fastlane to accomplish two tasks:</p><ul><li>Build and release the app running in a staging environment.</li><li>Build and release the app running in a production environment.</li></ul><p>The difference between those two methods is merely the configuration. The shared tasks are:</p><ul><li>Sign the code with the certificate and the provisioning profile</li><li>Build and export the app</li><li>Upload the app to Crashlytics (or other distribution platform)</li></ul><p>Knowing our tasks, we can start to write the fastlane script now. We will use the fastlane for Swift to write our script in this project. The fastlane for Swift is still in beta, so everything works well except:</p><ul><li>It doesn&#x2019;t support plugins</li><li>It doesn&#x2019;t catch exceptions</li></ul><p>But writing the script in Swift makes it more readable and maintainable for the developers. And you are able to convert the Swift script to Ruby script with ease. So let&#x2019;s try it!</p><p>We first start our project (still remember the Bundler?):</p><p>bundler exec fastlane init swift</p><p>Then, you are able to find a script in fastlane/Fastfile.swift. In the script, there&#x2019;s a fastfile class. It&#x2019;s our main program. Every method named with postfix, &#x201C;<strong>Lane</strong>&#x201D;, in this class is a lane. We can add predefined actions to a lane, and execute the lane with a command:</p><p>bundle exec fastlane &lt;lane name&gt;.</p><p>Let&#x2019;s fill in some code:</p><p>We create two lanes: <strong>developerRelease</strong> and <strong>qaRelease</strong> for our tasks. Both tasks do the same thing: build a package with the specific configuration and upload the exported ipa to Crashlytics.</p><p>There&#x2019;s a method package in both lane. The interface of <em>package()</em> looks like:</p><p>The parameter is an object conforming the protocol Configuration. The definition of the Configuration is:</p><p>Then we create two structs conforming the protocol:</p><p>Using the protocol, we are able to make sure every configuration comes with required settings. And we don&#x2019;t need to write the package detail every time when we have a new configuration.</p><p>So how does the <em>package(config:)</em> looks like? First, it needs to import the certificate from the file system. Remember our code signing folder? We use <a href="https://docs.fastlane.tools/actions/import_certificate/" rel="noopener">importCertificate</a> action to achieve our goal.</p><p>keychainName is the name of your Keychain, the default one is called &#x201C;login&#x201D;. The <em>keychainPassword</em> is the password to your Keychain, fastlane uses it to unlock your Keychain. Since we commit the Fastfile.swift to the repository to make sure the delivery code is consistent in every machine, it&#x2019;s a bad idea to write the passwords as string literals in the Fastfile.swift. Therefore, we use environment variable to replace the string literal. In the system, we save environment variable by:</p><p>export KEYCHAIN_NAME=&#x201D;KEYCHAIN_NAME&#x201D;;<br>export KEYCHAIN_PASSWORD=&#x201D;YOUR_PASSWORD&#x201D;;</p><p>In the Fastfile, we use <em>environmentVariable(get:)</em> to get the value of the environment variable. By using the environment variable, we can avoid showing the password in the code and greatly improve the security.</p><p>Back to the <em>importCertificate()</em>, the <em>certificatePath</em> is the path of your .p12 certificate file. We create a enum named &#x201C;<em>ProjectSetting</em>&#x201D; to keep the shared project setting. Here we also use the environment variable to pass the password.</p><p>After importing the certificate, we are going to set up the provisioning profile. We use <a href="https://docs.fastlane.tools/actions/update_project_provisioning/" rel="noopener">updateProjectProvisioning</a>:</p><p>This action gets the provisioning profile, imports it and modifies your project setting in the specified configuration. The profile parameter is the path to the provisioning profile. The target filter uses regular expression notation to find the target that we want to modify. Note that the updateProjectProvisioning does modify your project file, so please be careful if you want to run it on your local machine. It doesn&#x2019;t matter to CD task since the CD system won&#x2019;t commit any change to the repository.</p><p>Okay, we finished the code signing part! The following part would be quite straightforward, so bear with me!</p><p>Let&#x2019;s build an app now:</p><p><a href="https://docs.fastlane.tools/actions/build_app/" rel="noopener">buildApp</a> helps you build and export your project. It calls <strong>xcodebuild</strong> under the hood. Every parameter is intuitive except the <em>exportOptions</em>. Let take a look at it&#xFF1A;</p><p>Unlike other parameters, it&#x2019;s a dictionary. &#x201C;<em>signingStyle</em>&#x201D; is how you want to sign your code, we put &#x201C;<em>manual</em>&#x201D; here. &#x201C;<em>provisioningProfiles</em>&#x201D; is also a dictionary. It&#x2019;s the mapping between the app id and the corresponding provisioning profile. Finally we finished the fastlane setup! Now you can do this:</p><p>bundle exec fastlane qaRelease</p><p>or this:</p><p>bundle exec fastlane developerRelease</p><p>to release test build with proper configurations!</p><h3 id="jenkins%E2%80%99ll-be-your-server-for-tonight">Jenkins&#x2019;ll be your server for tonight</h3><p>Jenkins is an automation server that helps you to perform the CI/CD tasks. It runs a web GUI interface and is pretty easy to customize, so it&#x2019;s a great choice for an agile team. The rule of the Jenkins in our project can be depicted in the following graph:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*9grv9Y-KdYv5vHGk." class="kg-image" alt loading="lazy"></figure><p>The Jenkins fetches the latest code of the project and runs tasks periodically for you. In the execute shell section, we can see that Jenkins actually performs the task that we just did in the previous sections. But now we don&#x2019;t need to do them ourselves, Jenkins does this for you seamlessly!</p><p>Start from the nightly build job, let&#x2019;s start to create a Jenkins task. First, we create a &#x201C;freestyle project&#x201D;, and enter the &#x201C;Configure&#x201D; page of it. The first thing we need to configure is the <strong>Source Code Management</strong>(SCM) section:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*6txUjxhUml5zC1wb." class="kg-image" alt loading="lazy"></figure><p><strong>Repository URL</strong> is the source code url of the project. If your repository is a private one, you need to add <strong>Credentials</strong> to get the access to the repository. You can set target branch in the <strong>Branches to build</strong>, usually it&#x2019;s your default branch.</p><p>Then, below we can see <strong>Builder Trigger</strong> section. In this section we can decide what&#x2019;s going to be the trigger of the build job. According to our workflow, we want it to start every weeknight.</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*I-YHW-1sJ44wooCR." class="kg-image" alt loading="lazy"></figure><p>So we check the <strong>Poll SCM</strong>, it means that the Jenkins will poll the designated repository periodically. In the schedule text area:</p><p>H 0 * * 0&#x2013;4</p><p>What does it mean? Let&#x2019;s check the official instruction:</p><blockquote><br><br><br><br><br><br></blockquote><p>It consists of five fields:</p><ul><li>minute</li><li>hour</li><li>day</li><li>month</li><li>weekday</li></ul><p>The field could be a number. We can also use &#x201C;*&#x201D; to represent &#x201C;all&#x201D; numbers. And we use &#x201C;H&#x201D; to represent a hash, auto-selected &#x201C;one&#x201D; number.</p><p>So our schedule:</p><p>H 0 * * 0&#x2013;4</p><p>means: the job runs at certain minute from 0am to 1am every night, from Sunday to Thursday.</p><p>Last but not least, let&#x2019;s check the <strong>Build</strong> section below. Here&#x2019;s the task we want Jenkins to execute:</p><p>export LC_ALL=en_US.UTF-8;<br>export LANG=en_US.UTF-8;export CODESIGNING_PATH=&#x201D;/path/to/cert&#x201D;;<br>export CERTIFICATE_PASSWORD=&#x201D;xxx&#x201D;;<br>export KEYCHAIN_NAME=&#x201D;XXXXXXXX&#x201D;;<br>export KEYCHAIN_PASSWORD=&#x201D;xxxxxxxxxxxxxx&#x201D;bundle install &#x2014; path vendor/bundler<br>bundle exec fastlane developerRelease</p><p>First 6 lines are setting the environment variables that we described before. And the 7th line installs the dependency, including the fastlane. Then the last line executes a lane named &#x201C;developerRelease&#x201D;. To sum up, this task builds and uploads a developerRelease every weekday night. This is our first nightly build! &#x1F680;</p><p>You can check the building status by clicking the build number in the side menu of a Jenkins project page:</p><figure class="kg-card kg-image-card"><img src="https://cdn-images-1.medium.com/max/1600/0*YFImLvOHvNHYCyfS." class="kg-image" alt loading="lazy"></figure><h3 id="summary">Summary</h3><p>Together with you we have learned how to create a CD system with fastlane and Jenkins. We understood how to manually manage the code signing. And we created a lane running the task for us automatically. We also explored how to switch the configuration without changing the code. Finally, we built a CD system that builds an app every night.</p><p>Although many iOS/macOS applications are created in one-man teams, automating the delivery process is still a high-leverage improvement. By automating the process, we can reduce the risk of delivering with a wrong configuration, avoid being stuck by expired code signing and reduce the waiting time of the build upload.</p><p>The workflow introduced in this article might not be exactly the same with yours, but it&#x2019;s really important to know that every team has its own workflow and pace. So you must create your own CD system to meet the need of your team. By using the techniques as building blocks, you must be able to build a new customized and better-fit CD system yourself!</p>]]></content:encoded></item><item><title><![CDATA[Nudge - 推出你的影響力]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/IMG_3157.JPG" class="kg-image" alt loading="lazy" width="2000" height="2667" srcset="https://huangshihting.works/blog/content/images/size/w600/2021/12/IMG_3157.JPG 600w, https://huangshihting.works/blog/content/images/size/w1000/2021/12/IMG_3157.JPG 1000w, https://huangshihting.works/blog/content/images/size/w1600/2021/12/IMG_3157.JPG 1600w, https://huangshihting.works/blog/content/images/size/w2400/2021/12/IMG_3157.JPG 2400w" sizes="(min-width: 720px) 720px"></figure><p><a href="https://www.books.com.tw/products/0010639316">&#x63A8;&#x51FA;&#x4F60;&#x7684;&#x5F71;&#x97FF;&#x529B;&#xFF1A;&#x6BCF;&#x500B;&#x4EBA;&#x90FD;&#x53EF;&#x4EE5;&#x5F71;&#x97FF;&#x5225;&#x4EBA;&#x3001;&#x6539;&#x5584;&#x6C7A;&#x7B56;&#xFF0C;&#x505A;&#x4EBA;&#x751F;&#x7684;&#x9078;&#x64C7;&#x8A2D;&#x8A08;&#x5E2B;</a></p><p>&#x9019;&#x672C;&#x66F8;&#x5728;&#x884C;&#x70BA;&#x7D93;&#x6FDF;&#x5B78;&#x4E2D;&#xFF0C;&#x4F54;&#x6709;&#x4E00;&#x5E2D;&#x4E4B;&#x5730;</p>]]></description><link>https://huangshihting.works/blog/nudge-tui-chu-ni-de-ying-xiang-li/</link><guid isPermaLink="false">61b94f8c3282d300016644ee</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 15 Dec 2021 02:19:47 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/IMG_3157.JPG" class="kg-image" alt loading="lazy" width="2000" height="2667" srcset="https://huangshihting.works/blog/content/images/size/w600/2021/12/IMG_3157.JPG 600w, https://huangshihting.works/blog/content/images/size/w1000/2021/12/IMG_3157.JPG 1000w, https://huangshihting.works/blog/content/images/size/w1600/2021/12/IMG_3157.JPG 1600w, https://huangshihting.works/blog/content/images/size/w2400/2021/12/IMG_3157.JPG 2400w" sizes="(min-width: 720px) 720px"></figure><p><a href="https://www.books.com.tw/products/0010639316">&#x63A8;&#x51FA;&#x4F60;&#x7684;&#x5F71;&#x97FF;&#x529B;&#xFF1A;&#x6BCF;&#x500B;&#x4EBA;&#x90FD;&#x53EF;&#x4EE5;&#x5F71;&#x97FF;&#x5225;&#x4EBA;&#x3001;&#x6539;&#x5584;&#x6C7A;&#x7B56;&#xFF0C;&#x505A;&#x4EBA;&#x751F;&#x7684;&#x9078;&#x64C7;&#x8A2D;&#x8A08;&#x5E2B;</a></p><p>&#x9019;&#x672C;&#x66F8;&#x5728;&#x884C;&#x70BA;&#x7D93;&#x6FDF;&#x5B78;&#x4E2D;&#xFF0C;&#x4F54;&#x6709;&#x4E00;&#x5E2D;&#x4E4B;&#x5730;&#xFF0C;&#x4E3B;&#x8981;&#x7684;&#x539F;&#x56E0;&#x4E00;&#x4E4B;&#xFF0C;&#x5927;&#x6982;&#x662F;&#x56E0;&#x70BA;&#x5B83;&#x662F;&#x4E00;&#x672C;&#x975E;&#x5E38;&#x5BB9;&#x6613;&#x95B1;&#x8B80;&#x7684;&#x66F8;&#x7C4D;&#xFF0C;&#x4F5C;&#x8005;&#x7684;&#x5BEB;&#x4F5C;&#x98A8;&#x683C;&#x975E;&#x5E38;&#x8F15;&#x9B06;&#x4F11;&#x9592;&#xFF0C;&#x4E5F;&#x4E0D;&#x904E;&#x4EFD;&#x4E3B;&#x89C0;&#xFF0C;&#x52A0;&#x4E0A;&#x4F8B;&#x5B50;&#x90FD;&#x662F;&#x5BE6;&#x969B;&#x6848;&#x4F8B;&#xFF0C;&#x8B80;&#x8D77;&#x4F86;&#x7279;&#x5225;&#x6709;&#x8DA3;&#x3002;</p><p>Nudge&#x662F;&#x8F15;&#x63A8;&#x7684;&#x610F;&#x601D;&#xFF0C;&#x5728;&#x9019;&#x672C;&#x66F8;&#x4E2D;&#x4E3B;&#x8981;&#x6307;&#x7A31;&#x5728;&#x4EBA;&#x5011;&#x505A;&#x6C7A;&#x7B56;&#x6642;&#xFF0C;&#x5F88;&#x5BB9;&#x6613;&#x53D7;&#x5230;&#x4E00;&#x4E9B;&#x5916;&#x5728;&#x56E0;&#x7D20;&#x7684;&#x5F71;&#x97FF;&#xFF0C;&#x800C;&#x9019;&#x4E9B;&#x5916;&#x5728;&#x56E0;&#x7D20;&#x6709;&#x6642;&#x5019;&#x5F88;&#x7C21;&#x55AE;&#xFF0C;&#x53EF;&#x80FD;&#x53EA;&#x662F;&#x63D0;&#x4F9B;&#x66F4;&#x7C21;&#x5316;&#x7684;&#x9078;&#x9805;&#x3001;&#x63D0;&#x4F9B;&#x9810;&#x8A2D;&#x9078;&#x9805;&#x7B49;&#x7B49;&#xFF0C;&#x5149;&#x662F;&#x9019;&#x4E9B;&#x7C21;&#x55AE;&#x7684;&#x8B8A;&#x5316;&#xFF0C;&#x5C31;&#x8DB3;&#x4EE5;&#x5F71;&#x97FF;&#x4EBA;&#x985E;&#x505A;&#x51FA;&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#x9078;&#x64C7;&#x3002;&#x76F8;&#x8F03;&#x65BC;&#x50CF;&#x6CD5;&#x5F8B;&#x3001;&#x5236;&#x5EA6;&#x3001;&#x898F;&#x7BC4;&#x7B49;&#x7B49;&#x5982;&#x679C;&#x4E0D;&#x8DDF;&#x8457;&#x7167;&#x4F5C;&#xFF0C;&#x5C31;&#x53EF;&#x80FD;&#x7121;&#x6CD5;&#x5728;&#x9019;&#x793E;&#x6703;&#x4E2D;&#x751F;&#x5B58;&#xFF0C;&#x8F15;&#x63A8;&#x7684;&#x4E2D;&#x5FC3;&#x601D;&#x60F3;&#x662F;&#xFF0C;&#x5E0C;&#x671B;&#x63D0;&#x4F9B;&#x4EBA;&#x5011;&#x5728;&#x9078;&#x64C7;&#x4E0A;&#x6700;&#x5927;&#x7684;&#x81EA;&#x7531;&#xFF0C;&#x4F46;&#x540C;&#x6642;&#x4E5F;&#x5F15;&#x5C0E;&#x4EBA;&#x5011;&#x9078;&#x64C7;&#x5C08;&#x5BB6;&#x5011;&#x8A8D;&#x70BA;&#x5C0D;&#x4ED6;&#x5011;&#x6BD4;&#x8F03;&#x6709;&#x5229;&#x7684;&#x9078;&#x9805;&#xFF0C;&#x5728;&#x9019;&#x672C;&#x66F8;&#x4E2D;&#xFF0C;&#x9019;&#x6A23;&#x7684;&#x505A;&#x6CD5;&#x88AB;&#x7A31;&#x70BA;&#x81EA;&#x7531;&#x5BB6;&#x9577;&#x5236;(libertarian paternalism)&#xFF0C;&#x76F8;&#x5C0D;&#x7684;&#x5F37;&#x5236;&#x4EBA;&#x5011;&#x9078;&#x64C7;&#x7684;&#x5236;&#x5EA6;&#xFF0C;&#x88AB;&#x7A31;&#x4E4B;&#x70BA;&#x5BB6;&#x9577;&#x5236;(paternalistic)&#x3002;</p><p>&#x6574;&#x672C;&#x66F8;&#x5927;&#x6982;&#x53EF;&#x4EE5;&#x5206;&#x6210;&#x5169;&#x500B;&#x90E8;&#x4EFD;&#xFF0C;&#x7B2C;&#x4E00;&#x500B;&#x90E8;&#x4EFD;&#x4E3B;&#x8981;&#x4ECB;&#x7D39;&#x793E;&#x6703;&#x5927;&#x773E;&#x662F;&#x591A;&#x9EBC;&#x5BB9;&#x6613;&#x88AB;&#x5F71;&#x97FF;&#xFF0C;&#x9019;&#x908A;&#x7684;&#x7406;&#x8AD6;&#x8DDF;<a href="https://www.books.com.tw/products/0010780181">&#x5FEB;&#x601D;&#x6162;&#x60F3;</a>&#x4E00;&#x6A23;&#xFF0C;&#x4EBA;&#x8166;&#x5728;&#x505A;&#x6C7A;&#x7B56;&#x6642;&#xFF0C;&#x901A;&#x5E38;&#x60C5;&#x611F;&#x56E0;&#x7D20;&#x5360;&#x4E86;&#x5F88;&#x5927;&#x4E00;&#x90E8;&#x4EFD;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x5FEB;&#x601D;&#x6162;&#x60F3;&#x4E2D;&#x7684;&#x7CFB;&#x7D71;&#x4E00;&#x7E3D;&#x662F;&#x80FD;&#x5920;&#x4E3B;&#x5C0E;&#x601D;&#x8003;&#x3002;&#x800C;&#x4E5F;&#x662F;&#x56E0;&#x70BA;&#x5982;&#x6B64;&#xFF0C;&#x5728;&#x653F;&#x5E9C;&#x63A8;&#x884C;&#x653F;&#x7B56;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x591A;&#x5C11;&#x53EF;&#x4EE5;&#x5229;&#x7528;&#x9019;&#x500B;&#x7279;&#x9EDE;&#xFF0C;&#x8B93;&#x653F;&#x7B56;&#x80FD;&#x5920;&#x5728;&#x4E0D;&#x640D;&#x5BB3;&#x81EA;&#x7531;&#x7684;&#x60C5;&#x6CC1;&#x4E0B;&#x88AB;&#x9806;&#x5229;&#x63A8;&#x884C;&#x3002;&#x7B2C;&#x4E8C;&#x90E8;&#x4EFD;&#x5C31;&#x958B;&#x59CB;&#x5229;&#x7528;&#x8A31;&#x591A;&#x771F;&#x5BE6;&#x6848;&#x4F8B;&#xFF0C;&#x8B1B;&#x8FF0;&#x9078;&#x64C7;&#x8A2D;&#x8A08;&#x5E2B;&#x5982;&#x4F55;&#x767C;&#x63EE;&#x8F15;&#x63A8;&#x7684;&#x529B;&#x91CF;&#xFF0C;&#x8B93;&#x7BA1;&#x7406;&#x8005;&#x80FD;&#x5920;&#x4E8B;&#x534A;&#x529F;&#x500D;&#x3002;&#x5982;&#x679C;&#x4F60;&#x662F;&#x5D07;&#x5C1A;&#x81EA;&#x7531;&#x610F;&#x5FD7;&#x7684;&#x4EBA;&#xFF0C;&#x53EF;&#x80FD;&#x6703;&#x89BA;&#x5F97;&#x9019;&#x6A23;&#x7684;&#x4F5C;&#x6CD5;&#x6709;&#x9EDE;&#x722D;&#x8B70;&#xFF0C;&#x4F46;&#x56DE;&#x6B78;&#x5230;&#x73FE;&#x5BE6;&#x9762;&#x4E0A;&#xFF0C;&#x5927;&#x591A;&#x6578;&#x7684;&#x793E;&#x6703;&#x5927;&#x773E;&#xFF0C;&#x662F;&#x7121;&#x6CD5;&#x5B8C;&#x5168;&#x7406;&#x6027;&#x601D;&#x8003;&#x7684;&#xFF0C;&#x5982;&#x679C;&#x6709;&#x5C08;&#x5BB6;&#x7684;&#x5354;&#x52A9;&#xFF0C;&#x5C0D;&#x65BC;&#x6574;&#x500B;&#x793E;&#x6703;&#x7684;&#x52A9;&#x76CA;&#x6703;&#x76F8;&#x7576;&#x5927;&#x3002;&#x5728;&#x7E3D;&#x7D50;&#x7684;&#x90E8;&#x4EFD;&#xFF0C;&#x4F5C;&#x8005;&#x4E5F;&#x8B93;&#x6211;&#x5011;&#x4E86;&#x89E3;&#x5230;&#xFF0C;&#x4E0D;&#x662F;&#x6240;&#x6709;&#x8B70;&#x984C;&#x90FD;&#x80FD;&#x5920;&#x900F;&#x904E;&#x81EA;&#x7531;&#x5BB6;&#x9577;&#x5236;&#x63A8;&#x884C;&#xFF0C;&#x4E5F;&#x4E0D;&#x662F;&#x81EA;&#x7531;&#x5BB6;&#x9577;&#x5236;&#x5C31;&#x5B8C;&#x5168;&#x6C92;&#x554F;&#x984C;&#xFF0C;&#x4F46;&#x662F;&#x5F9E;&#x904E;&#x5F80;&#x7684;&#x7D93;&#x9A57;&#x4F86;&#x770B;&#xFF0C;&#x7D55;&#x5C0D;&#x662F;&#x4E00;&#x500B;&#x6709;&#x6548;&#x800C;&#x4E14;&#x6548;&#x679C;&#x986F;&#x8457;&#x7684;&#x65B9;&#x6CD5;&#x3002;</p><p>&#x9019;&#x672C;&#x66F8;&#x7684;&#x6982;&#x5FF5;&#x975E;&#x5E38;&#x5730;&#x4E2D;&#x6027;&#x4E26;&#x4E14;&#x53EF;&#x884C;&#xFF0C;&#x81EA;&#x7531;&#x5BB6;&#x9577;&#x5236;&#x4E5F;&#x5728;&#x5B8C;&#x5168;&#x81EA;&#x7531;&#x610F;&#x5FD7;&#x8DDF;&#x5B8C;&#x5168;&#x7BA1;&#x5236;&#x4E2D;&#xFF0C;&#x53D6;&#x5F97;&#x4E86;&#x4E00;&#x500B;&#x4E0D;&#x932F;&#x7684;&#x5E73;&#x8861;&#xFF0C;&#x5C31;&#x73FE;&#x5BE6;&#x4F86;&#x8AAA;&#xFF0C;&#x81EA;&#x7531;&#x5BB6;&#x9577;&#x5236;&#x5728;&#x6548;&#x7387;&#x4E0A;&#x6216;&#x8A31;&#x4E0D;&#x5982;&#x5F37;&#x5236;&#x57F7;&#x884C;&#x90A3;&#x6A23;&#x6709;&#x6548;&#xFF0C;&#x4F46;&#x4FDD;&#x7559;&#x4E86;&#x6700;&#x5927;&#x7684;&#x81EA;&#x7531;&#x7D66;&#x6301;&#x6709;&#x4E0D;&#x540C;&#x610F;&#x898B;&#x7684;&#x7FA4;&#x9AD4;&#xFF0C;&#x500B;&#x4EBA;&#x89BA;&#x5F97;&#x4E0D;&#x8AD6;&#x653F;&#x5E9C;&#x6216;&#x662F;&#x516C;&#x53F8;&#xFF0C;&#x5728;&#x7BA1;&#x7406;&#x4E0A;&#x90FD;&#x5F88;&#x9700;&#x8981;&#x518D;&#x591A;&#x82B1;&#x5FC3;&#x529B;&#xFF0C;&#x5728;&#x80FD;&#x5920;&#x8F15;&#x63A8;&#x7684;&#x5730;&#x65B9;&#xFF0C;&#x65BD;&#x52A0;&#x4E00;&#x9EDE;&#x529B;&#x9053;&#xFF0C;&#x964D;&#x4F4E;&#x6574;&#x500B;&#x7FA4;&#x9AD4;&#x4E43;&#x81F3;&#x793E;&#x6703;&#x7684;&#x6574;&#x9AD4;&#x6210;&#x672C;&#x3002;</p><p></p>]]></content:encoded></item><item><title><![CDATA[槍砲、病菌與鋼鐵：人類社會的命運 - Guns, Germs, and Steel: The Fates of Human Societies]]></title><description><![CDATA[<h1></h1><p>&#x9019;&#x662F;&#x4E00;&#x672C;&#x4E2D;&#x6587;&#x7248;&#x7E3D;&#x5171;&#x6709;&#x516D;&#x5343;&#x591A;&#x9801;&#x7684;&#x9245;&#x4F5C;&#xFF0C;&#x7531;&#x751F;&#x7269;&#x5730;&#x7406;&#x5B78;&#x5BB6; Jared Diamond &#x64B0;&#x5BEB;&#x800C;&#x6210;&#x3002;&#x9019;&#x672C;&#x66F8;&#x5149;&#x662F;&#x95B1;&#x8B80;&#x5C31;&#x82B1;&#x4E86;&#x6211;&#x4E09;&#x500B;&#x6708;&#x7684;&#x6642;&#x9593;&#xFF08;</p>]]></description><link>https://huangshihting.works/blog/qiang-pao-bing-jun-yu-gang-tie-ren-lei-she-hui-de-ming-yun-guns-germs-and-steel-the-fates-of-human-societies/</link><guid isPermaLink="false">61b94f553282d300016644e7</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 15 Dec 2021 02:14:09 GMT</pubDate><content:encoded><![CDATA[<h1></h1><p>&#x9019;&#x662F;&#x4E00;&#x672C;&#x4E2D;&#x6587;&#x7248;&#x7E3D;&#x5171;&#x6709;&#x516D;&#x5343;&#x591A;&#x9801;&#x7684;&#x9245;&#x4F5C;&#xFF0C;&#x7531;&#x751F;&#x7269;&#x5730;&#x7406;&#x5B78;&#x5BB6; Jared Diamond &#x64B0;&#x5BEB;&#x800C;&#x6210;&#x3002;&#x9019;&#x672C;&#x66F8;&#x5149;&#x662F;&#x95B1;&#x8B80;&#x5C31;&#x82B1;&#x4E86;&#x6211;&#x4E09;&#x500B;&#x6708;&#x7684;&#x6642;&#x9593;&#xFF08;&#x7576;&#x7136;&#x4E2D;&#x9593;&#x6709;&#x4EA4;&#x932F;&#x770B;&#x4E86;&#x5E7E;&#x672C;&#x5176;&#x5B83;&#x7684;&#x66F8;&#x63DB;&#x63DB;&#x5FC3;&#x60C5;XD&#xFF09;&#xFF0C;&#x66F4;&#x4E0D;&#x7528;&#x8AAA;&#x5C0D;&#x65BC;&#x4F5C;&#x8005;&#x4F86;&#x8AAA;&#xFF0C;&#x8981;&#x5B8C;&#x6210;&#x9019;&#x672C;&#x66F8;&#x9700;&#x8981;&#x591A;&#x9A5A;&#x4EBA;&#x7684;&#x6642;&#x9593;&#x3002;&#x539F;&#x672C;&#x5C0D;&#x65BC;&#x9019;&#x672C;&#x66F8;&#x6C92;&#x6709;&#x592A;&#x5927;&#x7684;&#x8208;&#x8DA3;&#xFF0C;&#x4EE5;&#x70BA;&#x5B83;&#x5C31;&#x662F;&#x53E6;&#x5916;&#x4E00;&#x672C;&#x5F8C;&#x898B;&#x4E4B;&#x660E;&#x7684;&#x6B77;&#x53F2;&#x8457;&#x4F5C;&#xFF0C;&#x4F46;&#x662F;&#x5728;&#x770B;&#x5B8C; Harari &#x7684;&#x300C;&#x4EBA;&#x985E;&#x5927;&#x6B77;&#x53F2;&#x300D;&#x4E4B;&#x5F8C;&#xFF0C;&#x89BA;&#x5F97;&#x96D6;&#x7136;&#x6B77;&#x53F2;&#x5F88;&#x96E3;&#x6B63;&#x78BA;&#x5730;&#x5206;&#x6790;&#xFF0C;&#x4F46;&#x662F;&#x9019;&#x985E;&#x65C1;&#x5FB5;&#x535A;&#x5F15;&#x3001;&#x7528;&#x5927;&#x91CF;&#x7684;&#x53F2;&#x6599;&#x8DDF;&#x7814;&#x7A76;&#x4F86;&#x652F;&#x6490;&#x8AD6;&#x9EDE;&#x7684;&#x8457;&#x4F5C;&#x537B;&#x662F;&#x610F;&#x5916;&#x7684;&#x975E;&#x5E38;&#x6709;&#x8DA3;&#x3002;&#x5C24;&#x5176;&#x662F;&#x300C;&#x4EBA;&#x985E;&#x5927;&#x6B77;&#x53F2;&#x300D;&#x96D6;&#x7136;&#x6709;&#x8DA3;&#xFF0C;&#x4F46;&#x662F;&#x770B;&#x5B8C;&#x4E4B;&#x5F8C;&#x9084;&#x662F;&#x5F88;&#x60F3;&#x518D;&#x7E7C;&#x7E8C;&#x4E86;&#x89E3;&#x50CF;&#x662F;&#x6771;&#x897F;&#x65B9;&#x5DEE;&#x7570;&#x3001;&#x8FD1;&#x4EE3;&#x6B77;&#x53F2;&#x7684;&#x7665;&#x7D50;&#x9EDE;&#x7B49;&#x7B49;&#xFF0C;&#x6240;&#x4EE5;&#x53C8;&#x53BB;&#x627E;&#x4E86;&#x9019;&#x672C;&#x66F8;&#x4F86;&#x770B;&#x3002;</p><p>&#x9019;&#x672C;&#x66F8;&#x4E00;&#x958B;&#x59CB;&#x5C31;&#x5148;&#x5F9E;&#x4EBA;&#x985E;&#x7684;&#x8FB2;&#x696D;&#x958B;&#x59CB;&#x8B1B;&#x8D77;&#x3002;&#x4EBA;&#x985E;&#x7684;&#x8FB2;&#x696D;&#x8D77;&#x6E90;&#x5730;&#x5206;&#x5E03;&#x5728;&#x4E16;&#x754C;&#x5404;&#x5730;&#xFF0C;&#x5176;&#x4E2D;&#x4EE5;&#x5169;&#x6CB3;&#x6D41;&#x57DF;&#x7684;&#x80A5;&#x6C83;&#x6708;&#x5F4E;&#x70BA;&#x6700;&#x65E9;&#x7684;&#x8FB2;&#x696D;&#x8D77;&#x6E90;&#x3001;&#x63A5;&#x8457;&#x767C;&#x751F;&#x5728;&#x53E4;&#x4EE3;&#x4E2D;&#x570B;&#x3001;&#x65B0;&#x5E7E;&#x5167;&#x4E9E;&#x7136;&#x5F8C;&#x6162;&#x6162;&#x904D;&#x5E03;&#x5230;&#x4E16;&#x754C;&#x5404;&#x5730;&#x3002;&#x4F46;&#x662F;&#x4F60;&#x6709;&#x6C92;&#x6709;&#x60F3;&#x904E;&#xFF0C;&#x4E16;&#x754C;&#x5404;&#x5730;&#x7684;&#x8FB2;&#x696D;&#x90FD;&#x662F;&#x7368;&#x7ACB;&#x767C;&#x5C55;&#xFF0C;&#x9084;&#x662F;&#x5F9E;&#x80A5;&#x6C83;&#x6708;&#x5F4E;&#x958B;&#x59CB;&#x6162;&#x6162;&#x50B3;&#x5230;&#x4E16;&#x754C;&#x5404;&#x5730;&#xFF1F;&#x5F9E;&#x8FB2;&#x696D;&#x518D;&#x884D;&#x4F38;&#x5230;&#x8FD1;&#x4EE3;&#xFF0C;&#x70BA;&#x751A;&#x9EBC;&#x6709;&#x4E9B;&#x570B;&#x5BB6;&#x5728;&#x897F;&#x5143; 1600 &#x5F8C;&#x6210;&#x70BA;&#x5E1D;&#x570B;&#xFF0C;&#x800C;&#x6709;&#x4E9B;&#x570B;&#x5BB6;&#x537B;&#x4E00;&#x76F4;&#x5230;&#x8FD1; 20 &#x5E74;&#x624D;&#x958B;&#x59CB;&#x812B;&#x96E2;&#x63A1;&#x96C6;&#x72E9;&#x7375;&#x7684;&#x539F;&#x59CB;&#x751F;&#x6D3B;&#xFF1F;&#x4E16;&#x754C;&#x5404;&#x5730;&#x7684;&#x4EA4;&#x6D41;&#x7D55;&#x5C0D;&#x4E0D;&#x662F;&#x8FD1;&#x4EE3;&#x624D;&#x958B;&#x59CB;&#x7684;&#xFF0C;&#x4F46;&#x662F;&#x5373;&#x4F7F;&#x5728;&#x4E00;&#x842C;&#x5E74;&#x524D;&#x4EBA;&#x985E;&#x5C31;&#x6709;&#x8DE8;&#x8D8A;&#x5927;&#x9678;&#x50B3;&#x64AD;&#x77E5;&#x8B58;&#x7684;&#x5148;&#x4F8B;&#xFF0C;&#x7F8E;&#x6D32;&#x5927;&#x9678;&#x7684;&#x50B3;&#x64AD;&#x5C31;&#x662F;&#x6BD4;&#x6B50;&#x4E9E;&#x5927;&#x9678;&#x7684;&#x50B3;&#x64AD;&#x66F4;&#x70BA;&#x7DE9;&#x6162;&#xFF0C;&#x9019;&#x53C8;&#x662F;&#x70BA;&#x751A;&#x9EBC;&#xFF1F;</p><p>&#x9019;&#x672C;&#x66F8;&#x7684;&#x6BCF;&#x4E00;&#x500B;&#x7AE0;&#x7BC0;&#x90FD;&#x8B93;&#x4EBA;&#x611F;&#x5230;&#x632F;&#x807E;&#x767C;&#x8075;&#xFF0C;&#x4E4B;&#x524D;&#x8B80;&#x6B77;&#x53F2;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x53EA;&#x89BA;&#x5F97;&#x80A5;&#x6C83;&#x6708;&#x5F4E;&#x8DDF;&#x4E2D;&#x570B;&#x90FD;&#x662F;&#x56E0;&#x70BA;&#x571F;&#x5730;&#x80A5;&#x6C83;&#x3001;&#x6C23;&#x5019;&#x689D;&#x4EF6;&#x4F73;&#xFF0C;&#x624D;&#x6709;&#x8FA6;&#x6CD5;&#x767C;&#x5C55;&#x51FA;&#x9F90;&#x5927;&#x7684;&#x8FB2;&#x696D;&#x793E;&#x6703;&#xFF0C;&#x4F46;&#x662F;&#x986F;&#x7136;&#x9019;&#x5169;&#x9805;&#x689D;&#x4EF6;&#x9084;&#x4E0D;&#x5920;&#xFF0C;&#x571F;&#x5730;&#x6C23;&#x5019;&#x4F73;&#x7684;&#x5730;&#x65B9;&#x5728;&#x7576;&#x6642;&#x90A3;&#x500B;&#x5E74;&#x4EE3;&#x4E26;&#x4E0D;&#x662F;&#x53EA;&#x6709;&#x80A5;&#x6C83;&#x6708;&#x5F4E;&#x6216;&#x4E2D;&#x570B;&#x624D;&#x6709;&#xFF0C;&#x53EA;&#x4E0D;&#x904E;&#x9019;&#x4E9B;&#x65E9;&#x671F;&#x5C31;&#x767C;&#x5C55;&#x51FA;&#x8FB2;&#x696D;&#x7684;&#x793E;&#x6703;&#x6709;&#x500B;&#x975E;&#x5E38;&#x91CD;&#x8981;&#x7684;&#x5148;&#x6C7A;&#x689D;&#x4EF6;&#xFF0C;&#x5C31;&#x662F;&#x5728;&#x7576;&#x5730;&#x5782;&#x624B;&#x53EF;&#x5F97;&#x7684;&#x4F5C;&#x7269;&#xFF0C;&#x90FD;&#x662F;&#x9069;&#x5408;&#x99B4;&#x5316;&#x7684;&#x3002;&#x4EBA;&#x985E;&#x4E0D;&#x662F;&#x5728;&#x5E7E;&#x5343;&#x5E74;&#x524D;&#x7684;&#x67D0;&#x4E00;&#x5929;&#x5C31;&#x7A81;&#x7136;&#x5B78;&#x6703;&#x7684;&#x8FB2;&#x696D;&#xFF0C;&#x4E5F;&#x4E0D;&#x662F;&#x4E00;&#x89BA;&#x9192;&#x4F86;&#x5C31;&#x77E5;&#x9053;&#x90A3;&#x4E9B;&#x690D;&#x7269;&#x662F;&#x53EF;&#x4EE5;&#x99B4;&#x5316;&#x3001;&#x7A2E;&#x690D;&#x7684;&#xFF0C;&#x5982;&#x679C;&#x4F60;&#x8EAB;&#x908A;&#x7684;&#x690D;&#x7269;&#x90FD;&#x6840;&#x50B2;&#x4E0D;&#x99B4;&#xFF0C;&#x7576;&#x7136;&#x4F60;&#x7684;&#x793E;&#x6703;&#x4E5F;&#x5C31;&#x96E3;&#x4EE5;&#x767C;&#x5C55;&#x51FA;&#x8FB2;&#x696D;&#x3002;&#x4F5C;&#x8005;&#x5149;&#x662F;&#x5728;&#x99B4;&#x5316;&#x690D;&#x7269;&#x7684;&#x7AE0;&#x7BC0;&#xFF0C;&#x5C31;&#x82B1;&#x4E86;&#x4E00;&#x5343;&#x591A;&#x9801;&#x7684;&#x7BC7;&#x5E45;&#x5728;&#x8B1B;&#x89E3;&#x8FB2;&#x696D;&#x7684;&#x8D77;&#x6E90;&#x3001;&#x5404;&#x5730;&#x767C;&#x5C55;&#x5DEE;&#x7570;&#x7684;&#x9060;&#x56E0;&#xFF0C;&#x9084;&#x6709;&#x6642;&#x9593;&#x62C9;&#x5230;&#x897F;&#x5143;&#x5F8C;&#xFF0C;&#x5C0E;&#x81F4;&#x570B;&#x5BB6;&#x4E4B;&#x9593;&#x767C;&#x5C55;&#x5DEE;&#x7570;&#x7684;&#x8FD1;&#x56E0;&#xFF0C;&#x53EF;&#x4EE5;&#x8AAA;&#x975E;&#x5E38;&#x7684;&#x9245;&#x7D30;&#x9761;&#x907A;&#xFF0C;&#x4F46;&#x662F;&#x53C8;&#x4E0D;&#x6703;&#x50CF;&#x5B78;&#x8853;&#x8457;&#x4F5C;&#x90A3;&#x6A23;&#x6DF1;&#x5967;&#x96E3;&#x61C2;&#x3002;&#x63A5;&#x7E8C;&#x8FB2;&#x696D;&#x767C;&#x5C55;&#x7684;&#x5206;&#x6790;&#xFF0C;&#x4F5C;&#x8005;&#x5728;&#x5F8C;&#x9762;&#x7684;&#x7AE0;&#x7BC0;&#x7E7C;&#x7E8C;&#x628A;&#x6642;&#x9593;&#x63A8;&#x5230;&#x6587;&#x660E;&#x53F2;&#xFF0C;&#x627E;&#x51FA;&#x5404;&#x5730;&#x767C;&#x5C55;&#x5DEE;&#x7570;&#x7684;&#x9060;&#x8FD1;&#x56E0;&#xFF0C;&#x518D;&#x6162;&#x6162;&#x63A8;&#x5230;&#x897F;&#x5143; 1600 &#x958B;&#x59CB;&#x7684;&#x4E16;&#x754C;&#x5287;&#x8B8A;&#xFF0C;&#x9019;&#x5E7E;&#x500B;&#x7AE0;&#x7D50;&#x96D6;&#x7136;&#x6A6B;&#x8DE8;&#x4E86;&#x5404;&#x5927;&#x6D32;&#x3001;&#x7D93;&#x904E;&#x4E86;&#x5E7E;&#x5343;&#x5E74;&#xFF0C;&#x4F46;&#x662F;&#x4F5C;&#x8005;&#x537B;&#x80FD;&#x5920;&#x628A;&#x9019;&#x4E9B;&#x767C;&#x5C55;&#x7684;&#x56E0;&#x679C;&#x7DCA;&#x5BC6;&#x5730;&#x6263;&#x5728;&#x4E00;&#x8D77;&#xFF0C;&#x8B93;&#x4EBA;&#x975E;&#x5E38;&#x4F69;&#x670D;&#x4F5C;&#x8005;&#x7D44;&#x7E54;&#x9019;&#x6A23;&#x9F90;&#x5927;&#x53F2;&#x6599;&#x8DDF;&#x89C0;&#x9EDE;&#x7684;&#x80FD;&#x529B;&#x3002;</p><p>&#x883B;&#x591A;&#x4EBA;&#x6703;&#x628A;&#x300C;&#x4EBA;&#x985E;&#x5927;&#x6B77;&#x53F2;&#x300D;&#x8DDF;&#x300C;&#x69CD;&#x7832;&#x3001;&#x75C5;&#x83CC;&#x8207;&#x92FC;&#x9435;&#x300D;&#x5169;&#x672C;&#x66F8;&#x653E;&#x5728;&#x4E00;&#x8D77;&#x8AC7;&#xFF0C;&#x56E0;&#x70BA;&#x9019;&#x5169;&#x672C;&#x90FD;&#x662F;&#x5927;&#x53F2;&#x89C0;&#x7684;&#x8457;&#x4F5C;&#xFF0C;&#x7528;&#x975E;&#x5E38;&#x5B8F;&#x89C0;&#x7684;&#x89D2;&#x5EA6;&#x4F86;&#x770B;&#x904E;&#x5F80;&#x4EBA;&#x985E;&#x7684;&#x6B77;&#x53F2;&#x3002;&#x4E0D;&#x904E;&#x5F88;&#x660E;&#x986F;&#x300C;&#x4EBA;&#x985E;&#x5927;&#x6B77;&#x53F2;&#x300D;&#x7684;&#x8A31;&#x591A;&#x89C0;&#x9EDE;&#x90FD;&#x8DDF;&#x300C;&#x69CD;&#x7832;&#x3001;&#x75C5;&#x83CC;&#x8207;&#x92FC;&#x9435;&#x300D;&#x662F;&#x6709;&#x91CD;&#x758A;&#x7684;&#xFF0C;&#x800C;&#x5728;&#x9019;&#x4E9B;&#x91CD;&#x8986;&#x7684;&#x5730;&#x65B9;&#xFF0C;&#x300C;&#x69CD;&#x7832;&#x3001;&#x75C5;&#x83CC;&#x8207;&#x92FC;&#x9435;&#x300D;&#x53C8;&#x518D;&#x591A;&#x4E86;&#x6578;&#x500D;&#x4EE5;&#x4E0A;&#x7684;&#x8CC7;&#x6599;&#x5728;&#x5EFA;&#x69CB;&#x9019;&#x4E9B;&#x89C0;&#x9EDE;&#x3002;&#x9019;&#x4E5F;&#x8B93;&#x300C;&#x69CD;&#x7832;&#x3001;&#x75C5;&#x83CC;&#x8207;&#x92FC;&#x9435;&#x300D;&#x9019;&#x672C;&#x66F8;&#x6BD4;&#x300C;&#x4EBA;&#x985E;&#x5927;&#x6B77;&#x53F2;&#x300D;&#x8981;&#x96E3;&#x8B80;&#x4E0D;&#x5C11;&#xFF0C;&#x4F46;&#x80FD;&#x5920;&#x5F97;&#x5230;&#x7684;&#x89C0;&#x9EDE;&#x9084;&#x6709;&#x5206;&#x6790;&#x4E5F;&#x662F;&#x6578;&#x500D;&#x4E4B;&#x591A;&#x3002;&#x66F4;&#x8B93;&#x4EBA;&#x4F69;&#x670D;&#x7684;&#x662F;&#xFF0C;&#x4F5C;&#x8005;&#x5728;&#x9019;&#x9EBC;&#x5927;&#x91CF;&#x7684;&#x8CC7;&#x6599;&#x4E0A;&#x7D44;&#x7E54;&#x51FA;&#x4F86;&#x7684;&#x5167;&#x5BB9;&#x537B;&#x9084;&#x662F;&#x76F8;&#x7576;&#x6709;&#x689D;&#x7406;&#x4E26;&#x4E14;&#x6613;&#x61C2;&#x3002;&#x9019;&#x5169;&#x672C;&#x66F8;&#x6211;&#x89BA;&#x5F97;&#x90FD;&#x662F;&#x5927;&#x53F2;&#x89C0;&#x7684;&#x7D93;&#x9EDE;&#x9245;&#x4F5C;&#xFF0C;&#x4F46;&#x662F;&#x5982;&#x679C;&#x4E0D;&#x5BB3;&#x6015;&#x5927;&#x91CF;&#x6587;&#x5B57;&#x8DDF;&#x8CC7;&#x6599;&#x7684;&#x8A71;&#xFF0C;&#x6211;&#x6703;&#x975E;&#x5E38;&#x63A8;&#x85A6;&#x300C;&#x69CD;&#x7832;&#x3001;&#x75C5;&#x83CC;&#x8207;&#x92FC;&#x9435;&#x300D;&#xFF0C;&#x5B83;&#x5E36;&#x4F86;&#x7684;&#x9707;&#x61BE;&#x771F;&#x7684;&#x4E0D;&#x662F;&#x5176;&#x5B83;&#x6B77;&#x53F2;&#x8457;&#x4F5C;&#x80FD;&#x5920;&#x6BD4;&#x64EC;&#x7684;&#x3002;</p>]]></content:encoded></item><item><title><![CDATA[Dive deep into Swift String]]></title><description><![CDATA[<h1></h1><p>String type is a fundamental type of all programming languages. It&#x2019;s also the very first language that you will learn when start programming. The String type is easy and straightforward. However, on the other hand, it is very complicated under the hood. The String type needs to handle</p>]]></description><link>https://huangshihting.works/blog/dive-deep-into-swift-string/</link><guid isPermaLink="false">61f6686d56cf0e000144187e</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 09 Jan 2019 15:00:00 GMT</pubDate><content:encoded><![CDATA[<h1></h1><p>String type is a fundamental type of all programming languages. It&#x2019;s also the very first language that you will learn when start programming. The String type is easy and straightforward. However, on the other hand, it is very complicated under the hood. The String type needs to handle the glyphs of various languages, the reading order of languages, and many other locale-specific behaviors. Moreover, the storage and the display of a String in the modern computer system is also a big issue. All of those are elegantly wrapped into an easy and intuitive String type.</p><p>When it comes to Swift, in Swift, the String type could be very intuitive, like concatenate Strings by using the &#x201C;+&#x201D; operator. It could also be very complicated, such as access a specific character in a string. Why String type is so controversial in Swift? Why can&#x2019;t we use the subscript pattern to access a character in the string? How does Swift fully support the Unicode and why does it matter? In this article, we will dive deep into the fundamental of the String type, from the String Encoding to the Swift String API. After reading this article, you will learn:</p><ul><li>What&#x2019;s String Encoding, Unicode, UTF-8, etc&#x2026;</li><li>How does Swift achieve the Unicode-compliance</li><li>Swift String API: index, comparison, substring, etc..</li><li>The relation between NSString and Swift String</li></ul><p>The Swift version of the code snippets is 4.2. But except the Swift String API, the fundamental concepts are invariable within different versions.</p><p>Let&#x2019;s start from the world outside Swift, back to the old good days &#x1F4BE;</p><h2 id="character-encoding">Character encoding</h2><p>As an engineer, you must be very familiar with the <a href="https://zh.wikipedia.org/zh-hant/ASCII">ASCII</a> code. Since the computer world is composed by 0 and 1, we must have an encoding system to represent the human characters by numbers. ASCII is a good example. The character &#x201C;A&#x201D; is encoded by a decimal number &#x201C;65&#x201D;, &#x201C;B&#x201D; is encoded by a decimal number &#x201C;66&#x201D;, and so on. The ASCII can represent 256 characters, including the whole 26 alphabetic characters. In that <a href="https://www.computerworld.com/article/2534312/operating-systems/the--640k--quote-won-t-go-away----but-did-gates-really-say-it-.html">640k-is-enough</a> era, it looked enough for encoding all English words. However, there are way more characters in languages other than English. The most common Korean characters is around 2,000. Not the mention the Chinese, which has over 50,000 characters. In order to make all languages in the world could be represented in the computer, we need a better encoding system. So the <a href="https://en.wikipedia.org/wiki/Unicode">Unicode</a> is here to rescue!</p><p>Before we jump into the Unicode system, let&#x2019;s take a deeper look at what&#x2019;s the Character Encoding. <a href="https://en.wikipedia.org/wiki/Character_encoding">Character Encoding</a> is a system used to represent characters by numbers. The purpose of doing encoding is because that we want to save, display, or distribute the language in the computer, and the computer can only deal with numbers. Intuitively, we can have a huge paper sheet, transcribing all characters and mark all of them with serial numbers. This huge paper sheet is called the &#x201C;Code Space&#x201D;. And the number assigned to each character is called the &#x201C;Code Point&#x201D;. Given this huge paper sheet, we can encode a paragraph of any language into a series of numbers, by looking up the Code Points for all characters in the paragraph. We can also load a series of numbers from a computer, and look up corresponding characters to decode them back to a paragraph. Briefly speaking, this imaginative huge paper sheet is the Unicode.</p><p>A single Unicode code point could be as small as 1, 2, 3&#x2026;, it could also be as large as 0x1F3C2 in Hex. The storage unit of a computer is by bytes (0~255). So you might wondering, if we need to save the code point into the computer storage, how many bytes do we need for a single code point? Let&#x2019;s take a look at an example:</p><p>&quot;&#x1F3C2; costs &#xA5;&quot;</p><p>This is a sentence with different special characters. The whole sentence can be represented by Unicode code points, as the following table:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/assets.001.png" class="kg-image" alt loading="lazy" width="780" height="460" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/assets.001.png 600w, https://huangshihting.works/blog/content/images/2022/01/assets.001.png 780w" sizes="(min-width: 720px) 720px"></figure><p>The first character, &#x1F3C2;, is an snowboarding emoji. In Unicode system, the code point of it is U+1F3C2, which is a 17-digit binary number 11111001111000010.</p><p>Now, we want to store those characters into the computer. Though the code points are numbers, in order to save those into the computer system, we need find a way to convert those numbers into bytes. Remember that the largest code point has 17 digits, so a 32-bit container is a good choice. One 32-bit container can store a English character such as &#x201C;c&#x201D; (code point: 99), it can also store a emoji &#x201C;&#x1F3C2;&#x201D; (code point: 127938). By using 32-bit containers, we can store all Unicode code points without any conversion. On one hand, it&#x2019;s convenient because we don&#x2019;t need to convert any code point, on the other hand, it wastes a lot of spaces. Most of common characters&#x2019; code points are only 8-bit, use 32-bit containers to save 8-bit data will leave 24 bits unused.</p><p>Another method is to use one 16-bit container to save a code point which is smaller than 16-bit, and use two 16-bit containers to save the one larger than 16-bit. The following graph depicts the relation between the code point and the size of different storage containers:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/img3.001.png" class="kg-image" alt loading="lazy" width="983" height="394" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/img3.001.png 600w, https://huangshihting.works/blog/content/images/2022/01/img3.001.png 983w" sizes="(min-width: 720px) 720px"></figure><p>Practically, we have to store the data bytes by bytes. In this graph, the &#x201C;abstract&#x201D; is the Unicode code point. The &#x201C;storage&#x201D; section is how we break down the code point into bytes. The way we encode the code point is called Character Encoding Form, and the container unit we are using is called Code Unit. A 17-bit code point can be encoded as one 32-bit code unit, or 4 8-bit code units. And as you may guess, the 32-bit character encoding form is <a href="https://en.wikipedia.org/wiki/UTF-32">UTF-32</a>, the 16-bit one is <a href="https://en.wikipedia.org/wiki/UTF-16">UTF-16</a>, and the last one is <a href="https://en.wikipedia.org/wiki/UTF-8">UTF-8</a>. We won&#x2019;t elaborate the detail of those character encoding forms, but the implementation of those are very interesting. You can check out more details, such as size of code spaces and how supplementary planes works in the wiki page.</p><p>Back to the graph above, now we are focusing on the UTF-16 and UTF-8. You may notice that we use two 16-bit code units and four 8-bit code units to represent the 17-bit code point. How about encoding the smaller code point? We use less code units! The table below shows the facts that, for UTF-16 and UTF-8, the length of code units for encoding a code point are variable. This fact is very important, we&#x2019;ll dig into this in the following section!</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/assets.005.png" class="kg-image" alt loading="lazy" width="939" height="463" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/assets.005.png 600w, https://huangshihting.works/blog/content/images/2022/01/assets.005.png 939w" sizes="(min-width: 720px) 720px"></figure><p>Now let&#x2019;s go back to the modern era, to see how Swift works with the String type.</p><h2 id="unicode-and-swift">Unicode and Swift</h2><p>Swift String type provides several Unicode-compliance API for working with various languages. In this section, we will introduce some handful Swift String APIs for dealing with Unicode or internationalization.</p><h3 id="string-literal">String Literal</h3><p>In Swift, we can initialize a String by giving the code point directly. For example:</p><pre><code class="language-swift">var stringLiteral = &#x201C;\u{1F30A}&#x201D; // &quot;&#x1F30A;&quot;
</code></pre><p>or,</p><pre><code class="language-swift">var stringLiteral2 = &#x201C;\u{8702}\u{871c}\u{6ab8}\u{6aac}&#x201D; // &quot;&#x8702;&#x871C;&#x6AB8;&#x6AAC;&quot;
</code></pre><h3 id="unicodescalars">UnicodeScalars</h3><p>You can also convert a string to code points:</p><pre><code class="language-swift">var scalars = &#x201C;&#x1F30A;&#x201D;.unicodeScalars // 127754, or 0x1F30A
</code></pre><p>The <code>unicodeScalars</code> is a Collection of <code>Unicode.Scalar</code>, means that you can do loop over it, or use the <code>map</code>, <code>filter</code> on it. A <a href="http://www.unicode.org/glossary/#unicode_scalar_value">Unicode.Scalar</a> represents a code point in Unicode, excluding the surrogate code points, which are functional code points used by UFT-16. Swift also provides functions for getting the code units of different character encoding form. Based on what we learn in the previous section, we won&#x2019;t be surprised to see the counts of code units vary among different encoding forms.</p><pre><code class="language-swift">let snowboardCostYen = &quot;&#x1F3C2; costs &#xFFE5;&quot;
snowboardCostYen.unicodeScalars     // count = 9
snowboardCostYen.utf16              // count = 10 
snowboardCostYen.utf8               // count = 14
</code></pre><h3 id="grapheme-cluster">Grapheme Cluster</h3><p>Generally, in Unicode, a character is represented by a single code point. But you can use more than one code point to represent a more complicated character. For example, the code point of an English character &#x201C;e&#x201D; is U+65. If we concatenate the U+65 with a acute code point U+301, we will get the e-acute character &#x201C;&#xE9;&#x201D;:</p><pre><code class="language-swift">let eCharacter = &quot;\u{65}&quot;             // &quot;e&quot;
let graphemeCluster = &quot;\u{65}\u{301}&quot; // &quot;&#xE9;&quot;
</code></pre><p>This kind of combination is called <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/Articles/stringsClusters.html">Grapheme Cluster</a>. A grapheme cluster is made by variable-length code points. A typical example of this is Hangul, a Hangul character could be made by three different Jamos:</p><pre><code class="language-swift">let first = &quot;\u{1112}&quot;                      // &quot;&#x1112;&quot;
let second = &quot;\u{1161}&quot;                     // &quot;&#x1161;&quot;
let third = &quot;\u{11AB}&quot;                      // &quot;&#x11AB;&quot;
let cluster = &quot;\u{1112}\u{1161}\u{11AB}&quot;    // &quot;&#xD55C;&quot;
</code></pre><p>Grapheme cluster is not necessarily composed by two code points, you can append more marks to a normal character to make a more complicated character:</p><pre><code class="language-swift">let enclose = &#x201C;A\u{20DD}\u{301}&#x201D;           // A&#x20DD;&#x301;
</code></pre><p>How complicated would it be? Here is a scaring example of <a href="https://www.urbandictionary.com/define.php?term=Zalgo">Zalgo</a>:</p><pre><code class="language-swift">var zalgo = &quot;Z&#x32B;&#x32B;&#x333;&#x330;&#x326;&#x359;&#x359;&#x312;&#x307;&#x33D;&#x312;&#x357;&#x311;&#x302;&#x30B;&#x31A;&#x1EA1;&#x348;&#x333;&#x31F;&#x319;&#x349;&#x325;&#x326;&#x319;&#x312;&#x314;&#x303;&#x34B;&#x305;&#x35B;&#x310;l&#x355;&#x32F;&#x31F;&#x356;&#x323;&#x34A;&#x313;&#x312;&#x302;&#x311;&#x33D;&#x30A;&#x303;&#x351;&#x303;&#x313;&#x33D;&#x1E21;&#x31F;&#x332;&#x35A;&#x31E;&#x346;&#x34A;&#x30B;&#x30F;&#x33D;&#x30F;&#x309;&#x345;&#xF4;&#x324;&#x348;&#x329;&#x316;&#x31E;&#x319;&#x332;&#x35A;&#x34D;&#x353;&#x318;&#x349;&#x342;&#x301;&#x306;&quot;
</code></pre><p>If you check the code points of this string by printing <code>zalgo.unicodeScalars</code>, you may find that it&#x2019;s made by five simple English characters, &#x201C;Zalgo&#x201D;, followed by a ton of combining marks. Currently, Swift supports the Extended Grapheme Clusters, you can check <a href="https://unicode.org/reports/tr29/">UNICODE TEXT SEGMENTATION</a> for more detail.</p><p>Here&#x2019;s another interesting fact of Swift string, let&#x2019;s print the count for each of the example above:</p><pre><code class="language-swift">let cluster = &quot;\u{1112}\u{1161}\u{11AB}&quot;    // &quot;&#xD55C;&quot;
let enclose = &#x201C;A\u{20DD}\u{301}&#x201D;           // A&#x20DD;&#x301;

print(cluster.count)     // 1
print(enclose.count)     // 1
</code></pre><p>Though the code points are more than one, the counts of those strings are still 1, which are consistent to the number of grapheme clusters in the string. Moreover, if you use a for loop to print the element of the string <code>cluster</code>, only one character &#x201C;&#xD55C;&#x201D; will be printed. All in all, in Swift, you can expect that the manipulation of the String type is as what you will see by human eyes.</p><p>Everything has two sides. To calculate the number of all grapheme clusters, we need to scan the whole string to determine which of those are clusters, which are not. Now you won&#x2019;t be shocked if I say the time complexity of the <code>.count</code> API is O(n) in Swift, and there&#x2019;s no random access to a Swift string!</p><p>Let&#x2019;s keep this fact in mind, we&#x2019;ll see more Swift String API related to this fact in the following section!</p><h2 id="string-api-in-swift">String API in Swift</h2><h3 id="string-index">String Index</h3><p>Now we know that Swift String cannot be random access, then how do we access a specific character in a string? We can use the <code>String.Index</code>:</p><pre><code class="language-swift">let snowboardCostsYen = &quot;&#x1F3C2; costs &#xA5;&quot;
print(snowboardCostsYen[8])      // Error!

let idx = snowboardCostsYen.index(snowboardCostsYen.startIndex, offsetBy: 8)
print(snowboardCostsYen[idx])    // &quot;&#xFFE5;&quot;
</code></pre><p>Using the Int subscript to a string infers random access, but using the String.Index subscript doesn&#x2019;t. Even though the usage here looks complicated, the semantic meaning of this API is clear: be careful, the time complexity of getting the index is not O(1)!</p><p>It&#x2019;s worth mentioning of some bight sides of the String API. Swift String provides many handful APIs for you. In <code>.index(i:, offsetBy:)</code> API, the <code>offsetBy</code> could be negative:</p><pre><code class="language-swift">let head = snowboardCostsYen.index(snowboardCostsYen.startIndex, offsetBy: 2)
let tail = snowboardCostsYen.index(snowboardCostsYen.endIndex, offsetBy: -3)
let costSubstring = snowboardCostsYen[head...tail] // costs
</code></pre><p>The <code>tail</code> in above&#x2019;s example indicates the third position from the end of string <code>snowboardCostsYen</code>, which is equivalent to <code>snowboardCostsYen.index(snowboardCostsYen.startIndex, offsetBy: 6)</code>.</p><p>If you want to look up the index of a specific character, you can use:</p><pre><code class="language-swift">let findIdxFirst = snowboardCostsYen.firstIndex(of: &quot;&#xFFE5;&quot;) 
let findIdxLast = snowboardCostsYen.lastIndex(of: &quot;&#xFFE5;&quot;) 
</code></pre><p>Those make the string index manipulation becomes very easy.</p><p>More detail about the String Index, especially for the design principle, check <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0180-string-index-overhaul.md">String Index Overhaul</a>.</p><h3 id="string-comparison">String Comparison</h3><p>Back to character, it&#x2019;s possible that two characters share the same glyph, while are composed by different code points. The e-acute &#x201C;&#xE9;&#x201D; is a good example:</p><pre><code class="language-swift">let acute1 = &quot;\u{E9}&quot;          // &quot;&#xE9;&quot;
let acute2 = &quot;\u{65}\u{301}&quot;   // &quot;&#xE9;&quot;
print(acute1 == acute2)        // true
</code></pre><p>In this case, acute1 and acute2 look like the same, but the code points are different. If we compare those two string, Swift returns <code>true</code>. Again, Swift makes the API consistent with the natural result.</p><p>A special case is the English &#x201C;A&#x201D; and the Russian &#x201C;&#x410;&#x201D;:</p><pre><code class="language-swift">*let* latinCapitalLetterA = &#x201C;\u{41}&#x201D;                    // &quot;A&quot;
*let* cyrillicCapitalLetterA = &#x201C;\u{0410}&#x201D;               // &quot;&#x410;&quot;   
print(latinCapitalLetterA == cyrillicCapitalLetterA)    // false 
</code></pre><p>Different from the e-acute case, these two characters are literally different, so it&#x2019;s reasonable that Swift returns <code>false</code>.</p><h3 id="substring">Substring</h3><p>From the example in the previous section:</p><pre><code class="language-swift">let head = snowboardCostsYen.index(snowboardCostsYen.startIndex, offsetBy: 2)
let tail = snowboardCostsYen.index(snowboardCostsYen.endIndex, offsetBy: -3)
let costSubstring = snowboardCostsYen[head...tail] // costs
</code></pre><p>If you check the type of the <code>costSubstring</code>, you may find that it&#x2019;s a <code>Substring</code> instead of a <code>String</code>. What&#x2019;s the differences between String and Substring? In order to improve the performance of the String, when getting the substring from a string, Swift actually doesn&#x2019;t allocate new memory space for the substring. Instead, it creates a range indicator, pointing out the range of the substring in the original string. The following graph depicts the relation:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/assets.006.png" class="kg-image" alt loading="lazy" width="642" height="362" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/assets.006.png 600w, https://huangshihting.works/blog/content/images/2022/01/assets.006.png 642w"></figure><p>The blue rectangle in the memory space occupied by the original string. When we create a substring, a new range indicator will be created, as the black hollow rectangle upon the blue one. No new memory space is needed. By doing this, substring manipulation in Swift is very fast and efficient. Besides, both String and Substring conform to the <code>StringProtocol</code>, so the usage of Substring is almost the same with the String. &#x1F44D;</p><p>Wait, you said the substring use the same memory space with the host string? So now two references are pointing to the same memory space? You bet! If you keep the reference to the Substring in somewhere else, the whole memory space of the original string will still be retained. As long as the substring reference is alive, space will be retained even when you remove the reference of the original string! Usually, Substring type is used as an intermediate. Means that it&#x2019;s better to use the substring in a local scope, such as in a function or a closure, and release it once we don&#x2019;t need it.</p><p>In <a href="https://github.com/apple/swift/blob/master/docs/StringManifesto.md#different-type-shared-storage">swift-evolution</a>, we can find more design mindset of the Substring.</p><h3 id="string-performance">String Performance</h3><p>Similar to the Substring, the memory techniques are used for the copy of strings as well. Given a string, strA, we create a new string variable, strB, and assign the strA to the strB:</p><pre><code class="language-swift">let strA = &quot;string A&quot;
var strB = strA

print(Unmanaged.passUnretained(strA as AnyObject).toOpaque())  // 0xbd0a6cb38041f74b
print(Unmanaged.passUnretained(strB as AnyObject).toOpaque())  // 0xbd0a6cb38041f74b
</code></pre><p>You will find that, even though the String is value type, they are sharing the same memory space! Now let&#x2019;s make some tweaks on the strB:</p><pre><code class="language-swift">strB.append(&quot;c&quot;)
print(Unmanaged.passUnretained(strB as AnyObject).toOpaque())  //0xbdc86da32414008a&quot;
</code></pre><p>The memory address of the strB changed! Here Swift plays a trick called <a href="https://en.wikipedia.org/wiki/Copy-on-write">Copy-on-write</a>. That is, if you&#x2019;re just assigning the string to a new variable without any modification, they will all share the same memory space. It makes the copy of string very fast and space-efficient. On the other hand, the new memory space is allocated only when the string is going to be modified. In most cases, strings are copied without modification, there&#x2019;s no point to allocate memory spaces if we are just reading them. This copy-on-write behavior works only behind the scenes, the interface behavior of a string remains the same as a value type.</p><h2 id="nsstring-vs-string">NSString v.s. String</h2><p>If you were born before 2014, or technically, you started writing iOS/macOS programs before 2014, you might be familiar with many NS-prefixed classes. Here we are going to discuss one of the NS-prefixed classes: NSString.</p><p>In Swift, a String could be just a Swift String, or it could be a NSString instance. Swift bridges the String and NSString, so you can simply use a type cast, &#x201C;as&#x201D;, to convert between those two types. Based on this, here&#x2019;s a very interesting case:</p><pre><code class="language-swit">let nStr: NSString = &quot;&#x1F3C2; costs &#xFFE5;&quot;
let sStr: String = nStr as String // Still NSString
let aNewStr = nStr + &quot;&quot;           // aNewStr is a String now
</code></pre><p>In the beginning, we define a NSString, cast it to a String. Now the compiler treats this sStr as a String, though it&#x2019;s still a NSString instance. By adding a &#x201C;&#x201D; by using &#x201C;+&#x201D; operator, aNewStr now is a String instance instead of a NSString instance. This observation is introduced by Ole Begemann in <a href="https://www.objc.io/blog/2017/12/12/quick-tip-for-string-performance/">objc.io</a>. This does matter because generally, the performance of a String instance is slightly better than the NSString instance.</p><p>Many of NSString methods do not be implemented in Swift String, but if you import the <em>Foundation</em> framework, you will have all the NSString methods bridged to the String type. The <code>.components(separatedBy:)</code> is an example:</p><pre><code class="language-swift">import Foundation
let sStr = &quot;&#x1F3C2; costs &#xFFE5;&quot;
sStr.components(separatedBy: &quot; &quot;)
</code></pre><p>About the encoding logic, NSString is different from the String. The encoding logic of the NSString is on the UTF-16 point of view, while the String is Unicode-based. What does it mean? If we check the length of a String and a NSString:</p><pre><code class="language-swift">let nStr: NSString = &quot;&#x1F3C2; costs &#xFFE5;&quot;
print(nStr.length) // 10

let sStr: String = nStr as String
print(sStr.count)  // 9
</code></pre><p>The length of the <code>sStr</code> is 9, which is the same as the Unicode characters. But the length of the <code>nStr</code> is 10, which is the same with the number of UTF-16 code units. Specifically, the emoji &#x201C;&#x1F3C2;&#x201D; is encoded into two code units in UTF-16.</p><p>Although the Swift String is Unicode-friendly, practically, a string still needs to be encoded by a character encoding form to be stored in a computer system. In Swift 4, the String uses the UTF-16 (sometimes ASCII for small strings) as the character encoding form. Again, this behavior works seamlessly, you don&#x2019;t even need to know how does Swift stores those values. All you need to memorize is that Swift String is Unicode-compliant, and that&#x2019;s it.</p><h2 id="summary">Summary</h2><p>In this article, we went through many aspects of the String type, including the character encoding, the API design, and the performance of it. Understand the character encoding is the key to the rationale of the Swift String API design. Swift is dedicated to making the usage of String intuitive and semantically clear. Besides, we also know that various techniques are adapted for improving the String&#x2019;s performance. Thought the Swift API is still changing (it changed rapidly &#x1F62C;), the logic and the mindset behind the API design should be the same. By knowing more implementation details of the Swift String, we are able to catch up the API changes and handle the String correctly in the future. &#x1F37B;</p><h2 id="references">References</h2><p><a href="https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html">Strings and Characters &#x2014; The Swift Programming Language (Swift 4.2)</a><br><a href="https://oleb.net/blog/2017/11/swift-4-strings/">Strings in Swift 4</a><br><a href="https://useyourloaf.com/blog/swift-string-cheat-sheet/">Swift String Cheat Sheet</a></p><h1></h1>]]></content:encoded></item><item><title><![CDATA[Advanced iOS tutorial on MVVM]]></title><description><![CDATA[<blockquote class="kg-blockquote-alt">How to use MVVM to tackle complicated TableView</blockquote><p>UITableView is definitely one of the most frequently used UI components for every iOS developer. Due to the size limitation of the mobile phone, table view becomes a great way to present information while keeping the clearness of the UI design.</p><p>In</p>]]></description><link>https://huangshihting.works/blog/advanced-ios-tutorial-on-mvvm/</link><guid isPermaLink="false">61f778b056cf0e000144189a</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Tue, 17 Jul 2018 15:00:00 GMT</pubDate><content:encoded><![CDATA[<blockquote class="kg-blockquote-alt">How to use MVVM to tackle complicated TableView</blockquote><p>UITableView is definitely one of the most frequently used UI components for every iOS developer. Due to the size limitation of the mobile phone, table view becomes a great way to present information while keeping the clearness of the UI design.</p><p>In the good old days, a table view contains typically cells displaying simple information such as a title with an image. And the only interaction is the selection of a cell.</p><p>But things change. Today, the table view is getting more and more complicated. Newsfeed, timeline, and wall have been widely adopted, a table view with various cells and asynchronous interactions is a new paradigm. So coding the complex UI is now challenging to developers.</p><p>In this article, we will talk how to organize your table view code with the Model-View-ViewModel (MVVM) pattern. MVVM is an architecture pattern that represents the view state using the data model. By making the UI logic a data model, we can use more Swift techniques. Such as protocol and closure to simplified the code in your table view. If you&#x2019;re not familiar with MVVM, I recommend you to check my previous <a href="https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b" rel="noopener">article</a> for a holistic introduction to the MVVM pattern.</p><h1 id="tddr">TD;DR</h1><p>In this article, you will learn how to:</p><ul><li><a href="https://medium.com/p/a2386ee817a9#7dee" rel="noopener">Newsfeed App</a></li><li><a href="https://medium.com/p/a2386ee817a9#8ecf" rel="noopener">Hand over the UI jobs</a></li><li><a href="https://medium.com/p/a2386ee817a9#a18e" rel="noopener">Create the ViewModel for cells</a></li><li><a href="https://medium.com/p/a2386ee817a9#f52d" rel="noopener">Use Protocol to Reduce the Redundancy</a></li><li><a href="https://medium.com/p/a2386ee817a9#965b" rel="noopener">Handle the business logic separately;</a></li><li><a href="https://medium.com/p/a2386ee817a9#f97d" rel="noopener">What to read if you want a deeper dive.</a></li></ul><p>Let&#x2019;s start! &#x1F4AA;</p><h1 id="newsfeed-app">Newsfeed App</h1><p>Now we are going to craft a newsfeed app. The home page of this app is a personalized &#x201C;wall.&#x201D; In the wall, there are two kinds of feeds, the photo feed, and the member feed. We&#x2019;re going to cover the following use cases:</p><ol><li>The tableView displays Member&#x2019;s cell and Photo&#x2019;s cell correctly.</li><li>The &#x201C;+&#x201D; button shows loading when waiting for the response of the follow-a-member API.</li><li>Trigger the open-detail event when users press the Photo cell.</li></ol><p>Here&#x2019;s a teaser:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/HCq_WiYTCnA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p></p><p><strong><strong><em><em>MemberCell</em></em></strong></strong><br>The member cell recommends members that you might want to follow. You can click on the &#x201C;+&#x201D; button in the cell. The cell has three states:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://huangshihting.works/blog/content/images/2022/01/image.png" class="kg-image" alt loading="lazy" width="1242" height="160" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/image.png 600w, https://huangshihting.works/blog/content/images/size/w1000/2022/01/image.png 1000w, https://huangshihting.works/blog/content/images/2022/01/image.png 1242w" sizes="(min-width: 720px) 720px"><figcaption>Normal</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://huangshihting.works/blog/content/images/2022/01/image-1.png" class="kg-image" alt loading="lazy" width="1242" height="160" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/image-1.png 600w, https://huangshihting.works/blog/content/images/size/w1000/2022/01/image-1.png 1000w, https://huangshihting.works/blog/content/images/2022/01/image-1.png 1242w" sizes="(min-width: 720px) 720px"><figcaption>Loading</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://huangshihting.works/blog/content/images/2022/01/image-2.png" class="kg-image" alt loading="lazy" width="1242" height="160" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/image-2.png 600w, https://huangshihting.works/blog/content/images/size/w1000/2022/01/image-2.png 1000w, https://huangshihting.works/blog/content/images/2022/01/image-2.png 1242w" sizes="(min-width: 720px) 720px"><figcaption>Checked</figcaption></figure><p></p><p><strong><strong><em><em>PhotoCell</em></em></strong></strong><br>The second type of the cell is a photo cell. In this cell, there are a title, description, and the photo itself. When users click on the cell, the app opens a corresponding photo detail view.</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/image-3.png" class="kg-image" alt loading="lazy" width="1242" height="273" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/image-3.png 600w, https://huangshihting.works/blog/content/images/size/w1000/2022/01/image-3.png 1000w, https://huangshihting.works/blog/content/images/2022/01/image-3.png 1242w" sizes="(min-width: 720px) 720px"></figure><p>We assume that the response of the API call is decoded into two models: <em><em>Member</em></em> and <em><em>Photo</em></em>.</p><p>If you&#x2019;re interested in how to make this app from zero to one, you can check my GitHub project <a href="https://github.com/koromiko/TheGreatWall" rel="noopener ugc nofollow">GitHub &#x2014; koromiko/TheGreatWall: Using MVVM to tackle complicated feed view</a>. There are more techniques for consolidating your table view, so remember to check it after reading!</p><h1 id="a-quick-look">A quick look</h1><p>Intuitively, we create one FeedListViewController containing a table view, make it as a dataSource and delegate of a table view. This is how the view controller looks like:</p><pre><code class="language-swift">var feeds: [Feed] = [Feed]()
var tableView: UITableView

func viewDidLoad() {
    self.service.fetchFeeds { [weak self] feeds in
        self?.feeds = feeds
        self?.tableView.reloadData()
    }
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
    return feeds.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
    let feed = feeds[indexPath.row]
    if let memberFeed = feed as? Member, let cell = tableView.dequeueReusableCell(withIdentifier: MemberCell.cellIdentifier(), for: indexPath) as? MemberCell {
        cell.nameLabel.text = memberFeed.name
        cell.profileImageView.image = memberFeed.image
        cell.addBtn.isSelected = memberFeed.isFollwing
        return cell
    } else if let photoFeed = feed as? Photo, let cell = tableView.dequeueReusableCell(withIdentifier: PhotoCell.cellIdentifier(), for: indexPath) as? PhotoCell {
        cell.titleLabel.text = photoFeed.captial
        cell.descriptionLabel.text = photoFeed.description
        cell.coverImageView.image = photoFeed.image
        return cell
    } else {
        fatalError(&quot;Unhandled feed \(feed)&quot;)
    }
}</code></pre><p>Then we need to handle the loading state of the Member cell. Normally, we use an array to save the loading state for all cells:</p><pre><code class="language-swift">var cellLoadingStatus: [Bool]</code></pre><p>Then update cells by using this:</p><pre><code class="language-swift">if cellLoadingStatus[indexPath.row] {
	cell.loadingIndicator.startAnimation()
} else {
	cell.loadingIndicator.stopAnimation()
}</code></pre><p>We have one more thing to do. The state update should be handled, like this:</p><pre><code class="language-swift">func updateLoadingState(isLoading: Bool, at indexPath: IndexPath) {
	cellLoadingStates[indexPath.row] = isLoading
	tableView.reloadRows(at indexPaths: [indexPath], with animation: .none)
}</code></pre><p>It seems to work fine.<br>Unfortunately, the requirement changed (as it can be in a real life). We are requested to add one more cell type, let&#x2019;s say, a place cell. What should we do? We&#x2019;ll add a cell class, add one more else-if statement to the <code>func tableView(tableView:,indexPath:) -&gt; UITableViewCell</code>. If the new cell has different states, we will add one more array to handle the state change and add a function to update the states. Phew! The fragmented code makes changing requirement a challenging task!</p><p>The following graph shows the architecture:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/image-4.png" class="kg-image" alt loading="lazy" width="727" height="480" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/image-4.png 600w, https://huangshihting.works/blog/content/images/2022/01/image-4.png 727w" sizes="(min-width: 720px) 720px"></figure><p>In the example above, the dataSource has the following responsibilities:</p><ol><li>Setups cells&#x2019; UI components such as the nameLabel, the imageView, etc.</li><li>Records cells&#x2019; states, e.g., cellLoadingStates.</li><li>Make an API call and save the model data.</li></ol><p>Whenever we add a cell type, we do those tasks in the FeedListViewController, which is not scalable at all. A better way to do this would be separating the responsibilities to improve the scalability. Adding cell type should be more like plugging a USB device to a computer, which won&#x2019;t change the design of the computer.<br><br>There is a lot of work to do, so let&#x2019;s start with a small step! &#x1F528;</p><h1 id="hand-over-the-ui-jobs">Hand over the UI jobs</h1><p>Back to the <code>func tableView(tableView:, cellForRowAt indexPath: ) -&gt; UITableViewCell</code>&#xFF0C;there&#x2019;re UI setup such as assigning the nameLabel.text and the imageView.image. It&#x2019;s not a good idea that dataSource knows the implementation detail of the cell. So our first step is to move the UI setup logic from the dataSource to the cell.</p><p>Based on this thought, we add an function to the MemberCell:</p><pre><code class="language-swift">// In MemberCell.swift
func setup(name: String, profileImage: UIImage, isFollowing: Bool, isLoading: Bool) {
	self.nameLabel.text = name
	self.profileImageView.image = profileImage
	self.addBtn.isSelected = isFollowing
	if isLoading { 
	    self.loadingIndicator.startAnimation()
	} else {
	    self.loadingIndicator.stopAnimation()
	}
}</code></pre><p>With the help of the setup function, we are able to simplified the <code>func tableView(tableView:, indexPath:) -&gt; UITableViewCell</code>, like this:</p><pre><code class="language-swift">if let memberFeed = feed as? Member, let cell = tableView.dequeueReusableCell(withIdentifier: MemberCell.cellIdentifier(), for: indexPath) as? MemberCell {
    cell.setup(name: memberFeed.name, 
               profileImage: memberFeed.image,
               isFollowing: memberFeed.isFollowing,
               isLoading: self.loadingStatus[indexPath.row])
    return cell
} else if let photoFeed = feed as? Photo, let cell = tableView.dequeueReusableCell(withIdentifier: PhotoCell.cellIdentifier(), for: indexPath) as? PhotoCell {
    cell.setup(title: photoFeed.title, 
               description: photoFeed.description,
               image: photoFeed.image)
    return cell
} else {
    fatalError(&quot;Unhandled feed \(feed)&quot;)
}</code></pre><p>We can see that there&#x2019;re functions with more than three parameters. It&#x2019;s better to have an object wrapping those parameters. Thus, here&#x2019;s the MVVM to rescue! MVVM helps you encapsulate all UI logic into a simple object.<br><br>By turning UI manipulation into object manipulation, we significantly reduce the overhead of the dataSource as well as the ViewController.</p><h1 id="create-the-viewmodel-for-cells">Create the ViewModel for cells</h1><p>Here&#x2019;s our architecture proposal:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/image-5.png" class="kg-image" alt loading="lazy" width="566" height="321"></figure><p>We plan to have two viewModels: MemberViewModel and PhotoViewModel, representing the MemberCell and the PhotoCell respectively. The ViewModel is a canonical form of the UI in a cell. We use bind techniques to bind the properties of the ViewModel to the UI components in the corresponding cell. It means that whenever we change a property of the ViewModel, the bond UI component in the cell changes responsively.</p><p>The implementation of the MemberViewModel looks like this:</p><pre><code class="language-swift">class MemberCellViewModel {
    let name: String
    let avatar: UIImage
    let isLoading: Observable&lt;Bool&gt;
}</code></pre><p>We use a customized object, Observable, as the binding tool. It keeps a generic type value and notifies the change of the stored value to the observer by triggering a closure named <code>valueChanged</code>. The implementation is pretty simple:</p><pre><code class="language-swift">class Observable&lt;T&gt; {
    var value: T {
        didSet {
            DispatchQueue.main.async {
                self.valueChanged?(self.value)
            }
        }
    }
    var valueChanged: ((T) -&gt; Void)?
}</code></pre><p>If you&#x2019;re interested in more detail about this binding techniques, please check the brilliant article <a href="https://five.agency/solving-the-binding-problem-with-swift/" rel="noopener ugc nofollow">Solving the binding problem with Swift &#x2022; Five</a> by Sr&#x111;an Ra&#x161;i&#x107;.</p><p>Back to our project, the corresponding MemberCell looks like this:</p><pre><code class="language-swift">// In MemberCell.swift
func setup(viewModel: MemberViewModel) {
    // The imange and the name won&apos;t be changed after setup 
    profileImageView.image = viewModel.image 
    nameLabel.text = viewModel.name
        
    // Listen to the change of the isLoading property to update the UI state
    viewModel.isLoading.valueChanged { [weak self] (isLoading) in
        if isLoading {
            self?.loadingIndicator.startAnimating()
        } else {
            self?.loadingIndicator.stopAnimating()
        }
    }
}</code></pre><p>In the <code>setup(viewModel:)</code> function, we set the UI components on the cell using the information from the viewModel parameter. Besides, we set the <code>valueChanged</code> closure to listen to the value change of the <code>isLoading</code>property. The <code>isLoading</code> becomes &#x201C;true&#x201D; when users press on the &#x201C;+&#x201D; button on the cell. By using MVVM, we simplify the parameter of the setup function. Moreover, converting the UI logic into object manipulation is of great benefit to more Swift techniques, which would be covered in the following section.</p><p>Here we have one more thing to do. In the cell, we use an escaping closure, <code>valueChanged</code>, to notify the cell of the UI update. Basically, the cell is reused when users scroll the table view. The relations between ViewModel and Cell could be represented by the following graph:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/image-6.png" class="kg-image" alt loading="lazy" width="556" height="333"></figure><p>Let&#x2019;s take a closer look at the <em><em>MemberViewModel 1</em></em>. In the beginning, the <em><em>MemberCell A</em></em> assign a closure to <code>valueChanged</code> of <em><em>MemberViewModel 1</em></em> to observe the value change. When users scroll the table view, the <em><em>MemberCell A</em></em> becomes invisible. The instant of <em><em>MemberCell A</em></em> then is reused for the following cells. However, since the <em><em>MemberViewModel 1</em></em> will not be released at this moment, the <code>valueChanged</code> of <em><em>MemberViewModel 1</em></em> is still notifying the <em><em>MemberCell A</em></em> (which is representing a different feed). That is, the <em><em>isLoading</em></em> property of <em><em>MemberViewModel 1</em></em> and <em><em>MemberViewModel 3</em></em> updates the same cell and the glitch happens!</p><p>The solution is straightforward:</p><pre><code class="language-swift">override func prepareForReuse() {
    super.prepareForReuse()
    viewModel?.isLoading.valueChanged = nil
}</code></pre><p>Unregister the observer from the ViewModel to avoid the glitch.</p><p>Now, it&#x2019;s time to clean up the dataSource!</p><pre><code class="language-swift">let viewModel = viewModels[indexPath.row]
if let memberViewModel = viewModel as? MemberViewModel, 
   let cell = tableView.dequeueReusableCell(withIdentifier: MemberCell.cellIdentifier(), for: indexPath) as? MemberCell {
    cell.setup(viewModel: memberViewModel)
    return cell
} else if let photoViewModel = viewModel as? PhotoViewModel, 
          let cell = tableView.dequeueReusableCell(withIdentifier: PhotoCell.cellIdentifier(), for: indexPath) as? PhotoCell {
    cell.setup(viewModel: photoViewModel)
    return cell
} else {
    fatalError(&quot;Unhandled feed \(feed)&quot;)
}</code></pre><p>The responsibility of the dataSource now is rather simple: setup the table view with cells&#x2019; ViewModels. On the other hand, the detail of setting up the UI components of cells are delegated to the cells. The states of cells are stored in the ViewModel (which is owned by the Controller instead of the ViewController). Isn&#x2019;t it clearer? &#x1F378;</p><p>Actually, there&#x2019;s still room for improvement! Since we unified the interface of the cell, we can reduce the redundancy by using the Protocol technique!</p><h1 id="use-protocol-to-reduce-the-redundancy">Use Protocol to Reduce the Redundancy</h1><p>In the <code>func tableView(tableView:, indexPath:) -&gt; UITableViewCell</code>, there are two setup(viewModel:) function calls, with different types of input view models. We can use a Swift Protocol to create a generic setup function for all kinds of cells. Let&#x2019;s create a protocol named <em><em>CellConfigurable</em></em>:</p><pre><code class="language-swift">protocol CellConfiguraable {
    func setup(viewModel: RowViewModel) // Provide a generic function
}</code></pre><p>This is a protocol of cells. The cell which confirms the protocol is able to be set up by using a RowViewModel instance. Meanwhile, the RowViewModel is another protocol:</p><pre><code class="language-swift">protocol RowViewModel {}
// make view models conform the RowViewModel protocol
class MemberViewModel: RowViewModel {...}
class PhotoViewModel: RowViewModel {...}</code></pre><p>The ViewModel conforms RowViewModel is able to be used to set up the CellConfigurable. By using the protocol, we are able to step further to clean up the dataSource:</p><pre><code class="language-swift">func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
    let rowViewModel = self.viewModels[indexPath.row]
    let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier(for: rowViewModel), for: indexPath)
    if let cell = cell as? CellConfiguraable {
        cell.setup(viewModel: rowViewModel)
    }
    return cell
}

/// Map the view model with the cell identifier (which will be moved to the Controller)
private func cellIdentifier(for viewModel: RowViewModel) {
    switch viewModel {
    case is PhotoCellViewModel:
        return PhotoCell.cellIdentifier()
    case is MemberCellViewModel:
        return MemberCell.cellIdentifier()
    default:
        fatalError(&quot;Unexpected view model type: \(viewModel)&quot;)
    }
}</code></pre><p>Now adding a cell type won&#x2019;t change the implementation of the <code>func tableView(tableView:, indexPath:) -&gt; UITableViewCell</code> anymore! In other words, the scalability of the dataSource becomes great: the complexity remaining the same no matter how many cells we add!</p><h1 id="more-protocol">More Protocol</h1><p>A similar technique could be used to deal with the cell interaction:</p><pre><code class="language-swift">protocol ViewModelPressible {
    var cellPressed: (()-&gt;Void)? { get set }
}
class PhotoViewModel: RowViewModel, ViewModelPressible {
    let title: String
    let desc: String
    var image: AsyncImage
var cellPressed: (() -&gt; Void)? // Conform the ViewModelPressible protocol
}</code></pre><p>We create a protocol named ViewModelPressible. There&#x2019;s a required implementation: a closure variable named <code>cellPressed</code>. We then make the PhotoViewModel conform the ViewModelPressible by adding a variable, <code>cellPressed</code>. Conforming the ViewModelPressible means that the ViewModel is able to handle the user-press event.</p><p>In the table view&#x2019;s delegate, we add an implementation of <code>func tableView(_ tableView:, indexPath:)</code>:</p><pre><code class="language-swift">func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let viewModel = self.viewModels[indexPath.row]
    if let viewModel = viewModel as? ViewModelPressible {
        rowViewModel.cellPressed?()
    }
}</code></pre><p>Now all didSelectRow events are transposed to the corresponding ViewModel. The following example shows the usage:</p><pre><code class="language-swift">// When creating the view model
let photoCellViewModel = PhotoCellViewModel()
photoCellViewModel.cellPressed = {
    print(&quot;Ask the coordinator or the view controller to open a photo viewer!&quot;)
}</code></pre><p>Again, we successfully wrap the interaction into a ViewModel object and keep the clarity of the delegate of the table view!<br><br>After finishing all those works, let&#x2019;s wipe the sweat off and zoom the camera out from the table view to the FeedListViewController! &#x1F52D;</p><h1 id="handle-the-business-logic-separately">Handle the business logic separately;</h1><p>You must notice that the FeedListViewController has one more unrelated job:</p><ol><li>Setups cells&#x2019; UI components such as the nameLabel, the imageView, etc.</li><li>Records cells&#x2019; states, e.g., cellLoadingStates.</li><li>Make an API call and save the model data.</li></ol><p>We need to recruit one more person to do this. Here&#x2019;s an ideal architecture:</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/image-7.png" class="kg-image" alt loading="lazy" width="746" height="444" srcset="https://huangshihting.works/blog/content/images/size/w600/2022/01/image-7.png 600w, https://huangshihting.works/blog/content/images/2022/01/image-7.png 746w" sizes="(min-width: 720px) 720px"></figure><p>We want to make the FeedListViewController a simple view, and move the business logic to another person: <em><em>FeedListController</em></em>. The FeedListController takes the responsibility of making API call and save the data models. We can say that the FeedListController&#x2019;s responsibility is to deal with the business logic. Based on this idea, let&#x2019;s create a FeedListController:</p><pre><code class="language-swift">// FeedListController.swift
let viewModels = Observable&lt;[RowViewModel]&gt;(value: [])

func start() {
    service.fetchFeed { [weak self] feeds in
        self?.buildViewModels(feeds: feeds)
    }
}

func buildViewModels(feeds: [Feed]) {
    var viewModels = [RowViewModel]()
    for feed in feeds {
        if let feed = feed as? Member {
            viewModels.append( MemberViewModel.from(feed) )
        } else if let feed = feed as? Photo {
            var photoViewModel = PhotoViewModel.from(feed)
            photoViewModel.cellPressed = { [weak self] in
                self?.handleCellPressed(feed)
            }
            viewModels.append(photoViewModel)
        }
    }
    self.viewModels.value = viewModels
}

func handleCellPressed(_ feed: Feed) {
    // Send analytics, fetch detail data, etc
    // Open detail photo view
}</code></pre><p>From the snippet above, we can see that the controller doesn&#x2019;t know the logic of any UI component. Instead, the ViewModel is created here as a middleman between the View and the Controller.</p><p>From another point of view, the FeedListViewController becomes:</p><pre><code class="language-swift">var viewModels: Observable&lt;[RowViewModel]&gt; {
    return controller.viewModels
}

override func viewDidLoad() {
    viewModels.valueUpdate { [weak self] (_) in
        self?.tableView.reloadData()
    }
    controller.start()
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
    return rowViewModels.value.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
    let rowViewModel = self.rowViewModels.value[indexPath.row]
    let cell = tableView.dequeueReusableCell(withIdentifier: controller.cellIdentifier(for: rowViewModel), for: indexPath)
    if let cell = cell as? CellConfiguraable {
        cell.setup(viewModel: rowViewModel)
    }
    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if let rowViewModel = rowViewModels.value[indexPath.row] as? ViewModelPressible {
        rowViewModel.cellPressed?()
    }
}</code></pre><p>The only responsibility of the FeedViewController is to set up the table view, serve as the dataSource and the delegate of the table view. Adding cell types or modify the interaction won&#x2019;t change this ViewController.</p><p>There are many advantages of doing this: it&#x2019;s possible to write the unit test to test UI states, the user interaction, as well as the server connection behavior! In addition to the improvement of the testability, the scalability of the whole feed module is also improved!</p><p>In this article, we showed how to simplify a complicated table view by doing those changes:</p><ol><li>Make the cell handles the UI components setup;</li><li>Use MVVM to abstract the UI components and interactions;</li><li>Use protocol to consolidate the setup interface;</li><li>Use Controller to handle to business logic.</li></ol><p>Massive View Controller is a notorious anti-pattern. Now we know that the problem is not the MVC pattern itself. There are many ways to separate the responsibilities of your system, including but not limited to the MVVM pattern. I would say the MVVM pattern is a handful tool to improve the code quality, but it&#x2019;s not a silver bullet. Besides of MVVM, we still need to take care of things such as the scalability, the testability and more principles like <a href="https://en.wikipedia.org/wiki/SOLID" rel="noopener ugc nofollow">SOLID &#x2014; Wikipedia</a>. So, before adopting any architecture, please thoroughly understand the design of the architecture, carefully select the one that&#x2019;s best fit your need.<br><br>Let&#x2019;s get our hands dirty and start to craft a better system! &#x1F37B;</p><h1 id="what-to-read-if-you-want-a-deeper-dive">What to read if you want a deeper dive.</h1><ul><li><a href="https://medium.com/@stasost/ios-how-to-build-a-table-view-with-multiple-cell-types-2df91a206429" rel="noopener">iOS: How to build a Table View with multiple cell types</a></li><li><a href="https://medium.cobeisfresh.com/dealing-with-complex-table-views-in-ios-and-keeping-your-sanity-ff5fee1fbb83" rel="noopener ugc nofollow">Dealing with Complex Table Views in iOS and Keeping Your Sanity</a></li><li><a href="https://academy.realm.io/posts/doios-natasha-murashev-protocol-oriented-mvvm/" rel="noopener ugc nofollow">Introduction to Protocol-Oriented MVVM</a></li><li><a href="https://blog.jayway.com/2016/11/15/clean-table-view-code-using-swift-protocols/" rel="noopener ugc nofollow">Clean Table View Code Using Swift Protocols &#x2014; Jayway</a></li></ul>]]></content:encoded></item><item><title><![CDATA[歡迎來到真實世界 - Continuous Delivery：在你睡覺的時候，電腦們可是都在勤奮地工作喔]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/img_7479.jpg" class="kg-image" alt="IMG_7479.jpg" loading="lazy"></figure><p>&#x5728;iOS&#x958B;&#x767C;&#x7684;&#x4E16;&#x754C;&#xFF0C;&#x6709;&#x500B;&#x975E;&#x5E38;&#x6709;&#x8DA3;&#xFF0C;&#x4F46;&#x4E5F;&#x975E;&#x5E38;&#x75DB;&#x82E6;&#x7684;&#x5730;&#x65B9;&#xFF0C;&#x5C31;&#x662F;iOS&#x7684;&#x958B;&#x767C;&#x8005;&#xFF0C;&#x5176;&#x5BE6;&#x9700;&#x8981;&#x7684;&#x57FA;&#x672C;&#x77E5;&#x8B58;&#x975E;&#x5E38;&#x5730;&#x591A;&#xFF0C;Cocoa framework &#x672C;</p>]]></description><link>https://huangshihting.works/blog/huan-ying-lai-dao-zhen-shi-shi-jie-continuous-delivery-zai-ni-shui-jue-de-shi-hou-dian-nao-men-ke-shi-du-zai-qin-fen-di-gong-zuo-wo/</link><guid isPermaLink="false">61f296f856cf0e000144176c</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Sun, 25 Mar 2018 13:30:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/img_7479.jpg" class="kg-image" alt="IMG_7479.jpg" loading="lazy"></figure><p>&#x5728;iOS&#x958B;&#x767C;&#x7684;&#x4E16;&#x754C;&#xFF0C;&#x6709;&#x500B;&#x975E;&#x5E38;&#x6709;&#x8DA3;&#xFF0C;&#x4F46;&#x4E5F;&#x975E;&#x5E38;&#x75DB;&#x82E6;&#x7684;&#x5730;&#x65B9;&#xFF0C;&#x5C31;&#x662F;iOS&#x7684;&#x958B;&#x767C;&#x8005;&#xFF0C;&#x5176;&#x5BE6;&#x9700;&#x8981;&#x7684;&#x57FA;&#x672C;&#x77E5;&#x8B58;&#x975E;&#x5E38;&#x5730;&#x591A;&#xFF0C;Cocoa framework &#x672C;&#x8EAB;&#x5C31;&#x6DB5;&#x84CB;&#x4E86;&#x524D;&#x7AEF;&#x7684;UI&#x908F;&#x8F2F;&#xFF0C;&#x8207;&#x8CC7;&#x6599;&#x5EAB;&#x7B49;&#x7B49;&#x7684;&#x5F8C;&#x7AEF;&#x908F;&#x8F2F;&#xFF0C;&#x65E2;&#x8981;&#x6CE8;&#x610F;&#x9801;&#x9762;&#x8DDF;&#x9801;&#x9762;&#x4E4B;&#x9593;&#x72C0;&#x614B;&#x7684;&#x8655;&#x7406;&#xFF0C;&#x4E5F;&#x8981;&#x5C0F;&#x5FC3;&#x8A18;&#x61B6;&#x9AD4;&#x7684;&#x904B;&#x7528;&#xFF0C;&#x6709;&#x6642;&#x5019;&#x9084;&#x8981;&#x5B78;&#x8C9D;&#x8332;&#x66F2;&#x7DDA;&#x8DDF; 3D &#x8F49;&#x5834;&#x3002;&#x96D6;&#x7136;&#x6BCF;&#x4E00;&#x6A23;&#x90FD;&#x4E0D;&#x53EF;&#x80FD;&#x50CF;&#x5404;&#x9818;&#x57DF;&#x7684;&#x5C08;&#x5BB6;&#x4E00;&#x6A23;&#x7CBE;&#x901A;&#xFF0C;&#x4F46;&#x4E5F;&#x7B97;&#x662F;&#x653B;&#x57CE;&#x7345;&#x88E1;&#x9762;&#x6B66;&#x5668;&#x76F8;&#x7576;&#x591A;&#x7684;&#x7A2E;&#x65CF;&#x4E86;&#x3002;&#x4ECA;&#x5929;&#xFF0C;&#x4E0D;&#x624D;&#x5C0F;&#x5F1F;&#x8981;&#x4F86;&#x5206;&#x4EAB;&#xFF0C;&#x8EAB;&#x70BA; iOS &#x5DE5;&#x7A0B;&#x5E2B;&#xFF0C;&#x4F60;&#x53EF;&#x80FD;&#x9084;&#x53EF;&#x4EE5;&#x591A;&#x5B78;&#x7684;&#x6280;&#x8853;&#xFF1A;Continous Delivery&#xFF01;&#x8EAB;&#x70BA;&#x4E00;&#x500B;&#x653B;&#x57CE;&#x7345;&#xFF0C;&#x4F60;&#x4E00;&#x5B9A;&#x6216;&#x591A;&#x6216;&#x5C11;&#x807D;&#x8AAA;&#x904E; Continous Intgration &#x8DDF; Continous Delivery (CI/CD)&#xFF0C;&#x4F46;&#x662F;&#x5BE6;&#x969B;&#x751F;&#x6D3B;&#x4E2D;&#xFF0C;&#x9664;&#x975E;&#x662F;&#x8DDF;&#x4E00;&#x500B;&#x5718;&#x968A;&#x4E00;&#x8D77;&#x958B;&#x767C;&#xFF0C;&#x4E0D;&#x7136;&#x61C9;&#x8A72;&#x5F88;&#x5C11;&#x6709;&#x6A5F;&#x6703;&#x6703;&#x78B0;&#x5230; CI/CD &#x7684;&#x6982;&#x5FF5;&#x3002;</p><p>&#x6240;&#x8B02;&#x7684; CI&#xFF0C;&#x5C31;&#x662F;&#x5728;&#x958B;&#x767C;&#x7684;&#x904E;&#x7A0B;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x96A8;&#x6642;&#x96A8;&#x5730;&#x90FD;&#x78BA;&#x4FDD;&#x6211;&#x5011;&#x7684; code &#x4E3B;&#x5E79;&#x90FD;&#x8655;&#x5728;&#x53EF;&#x4EE5;&#x4E00;&#x500B;&#x767C;&#x4F48;&#x7684;&#x72C0;&#x614B;&#x3002;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#xFF0C;&#x4E0D;&#x80FD;&#x56E0;&#x70BA;&#x6B63;&#x5728;&#x958B;&#x767C;&#x4E00;&#x500B;&#x65B0;&#x529F;&#x80FD;&#xFF0C;&#x6211;&#x5011;&#x7684;&#x4E3B;&#x5E79;&#x7A0B;&#x5F0F;&#x5C31;&#x7121;&#x6CD5;&#x904B;&#x4F5C;&#x6216;&#x662F;&#x7121;&#x6CD5;&#x6253;&#x5305;&#x65B0;&#x7248;&#x672C;&#x3002;&#x800C; CD&#xFF0C;&#x6307;&#x7684;&#x5247;&#x662F;&#xFF0C;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x5728;&#x958B;&#x767C;&#x7684;&#x4EFB;&#x4F55;&#x4E00;&#x500B;&#x968E;&#x6BB5;&#xFF0C;&#x90FD;&#x8981;&#x80FD;&#x5920;&#x81EA;&#x52D5;&#x5316;&#x6253;&#x5305;&#x51FA;&#x7248;&#x672C;&#xFF0C;&#x7D66;&#x9700;&#x8981;&#x7684;&#x4EBA;&#x4F7F;&#x7528;&#x3002;&#x8AB0;&#x6703;&#x662F;&#x9700;&#x8981;&#x7684;&#x4EBA;&#xFF1F;&#x5728;&#x958B;&#x767C;&#x7684;&#x904E;&#x7A0B;&#x4E2D;&#xFF0C;&#x5DE5;&#x7A0B;&#x5718;&#x968A;&#x60F3;&#x8981;&#x624B;&#x52D5;&#x6E2C;&#x8A66; app &#x6642;&#xFF0C;&#x5C31;&#x6703;&#x9700;&#x8981;&#x4E00;&#x500B; build &#x4F86;&#x6E2C;&#x8A66;&#xFF0C;&#x800C;&#x5728;&#x958B;&#x767C;&#x5B8C;&#x7562;&#x5F8C;&#xFF0C;UI&#x6E2C;&#x8A66;&#x4EBA;&#x54E1;&#x4E5F;&#x6703;&#x9700;&#x8981;&#x4E00;&#x500B; build &#x4F86;&#x505A;&#x6E2C;&#x8A66;&#x3002;&#x6700;&#x5F8C;&#xFF0C;&#x5728; APP &#x8981;&#x4E0A;&#x7DDA;&#x6642;&#xFF0C;&#x7406;&#x6240;&#x7576;&#x7136;&#x4E5F;&#x6703;&#x9700;&#x8981;&#x4E00;&#x500B; build &#x4F86;&#x9001;&#x5230; iTunesConnect &#x4E0A;&#x4EE5;&#x4F9B;&#x5BE9;&#x6838;&#x3002;</p><p>&#x6240;&#x4EE5;&#x5728;&#x7CFB;&#x5217;&#x4F5C;&#x62D6;&#x641E;&#x5C07;&#x8FD1;&#x4E09;&#x500B;&#x6708;&#x5F8C;&#xFF0C;&#x4ECA;&#x5929;&#x5728;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x7684;&#x6211;&#x5011;&#xFF0C;&#x8981;&#x4F86;&#x4ECB;&#x7D39;&#x4E00;&#x500B;&#x975E;&#x5E38;&#x5BE6;&#x7528;&#x7684;&#x6280;&#x5DE7;&#xFF0C;&#x5982;&#x4F55;&#x505A;&#x51FA;&#x4E00;&#x500B;&#x5C08;&#x4F9B; Apple &#x751F;&#x614B;&#x7CFB;&#x4F7F;&#x7528;&#x7684; CD &#x7CFB;&#x7D71;&#xFF0C;&#x8B93;&#x4F60;&#x53EF;&#x4EE5;&#x6BCF;&#x5929;&#x90FD;&#x63D0;&#x65E9;&#x4E94;&#x5206;&#x9418;&#x4E0B;&#x73ED;(&#x4E0D;&#x662F;&#x5F88;&#x5438;&#x5F15;&#x4EBA;)&#x3002;</p><p>&#x5982;&#x679C;&#x9084;&#x6C92;&#x770B;&#x904E;&#x7CFB;&#x5217;&#x4F5C;&#x7684;&#x524D;&#x9762;&#x5E7E;&#x96C6;&#xFF0C;&#x6B61;&#x8FCE;&#x53C3;&#x8003;&#x5C0F;&#x5F1F;&#x7684;<a href="https://huangshihting.works/blog">&#x9375;&#x76E4;&#x85CD;&#x7DA0;&#x85FB;Neo &#x2013; &#x4F86;&#x5BEB;&#x4E00;&#x4E9B;iOS&#x3001;&#x6280;&#x8853;&#x3001;&#x8207;&#x5783;&#x573E;&#x8A71;</a>&#xFF0C;&#x9019;&#x500B;&#x7CFB;&#x5217;&#x8A18;&#x8F09;&#x4E86;&#x6559;&#x79D1;&#x66F8;&#x6C92;&#x6709;&#x8A18;&#x8F09;&#x7684;&#x73FE;&#x5BE6;&#x751F;&#x6D3B;&#x958B;&#x767C;&#x6280;&#x8853;&#xFF0C;&#x6C92;&#x6709;&#x80CC;&#x666F;&#x97F3;&#x6A02;&#x4E5F;&#x6C92;&#x6709;&#x8DD1;&#x99AC;&#x71C8;&#x3002;</p><h2 id="outline">Outline</h2><p>&#x6211;&#x5011;&#x6703;&#x4EE5;&#x4E0B;&#x9762;&#x7684;&#x6B65;&#x9A5F;&#x4F86;&#x8AAA;&#x660E;&#x5982;&#x4F55;&#x5EFA;&#x7ACB;&#x4E00;&#x500B;&#x597D;&#x7528;&#x7684; CD &#x7CFB;&#x7D71;&#xFF0C;&#x4F60;&#x96A8;&#x6642;&#x90FD;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x8DF3;&#x5230;&#x67D0;&#x500B;&#x7AE0;&#x7BC0;&#x958B;&#x59CB;&#x95B1;&#x8B80;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x76F4;&#x63A5; End &#x770B;&#x96FB;&#x5F71;&#x5FC3;&#x5F97;&#xFF0C;&#x5225;&#x64D4;&#x5FC3;&#xFF0C;&#x5C0F;&#x5F1F;&#x5DF2;&#x7D93;&#x958B;&#x59CB;&#x601D;&#x8003;&#x53EA;&#x5BEB;&#x96FB;&#x5F71;&#x5FC3;&#x5F97;&#x7684;&#x53EF;&#x80FD;&#x6027;&#x3002;</p><ul><li>&#x5728;&#x9019;&#x500B; CD &#x7CFB;&#x7D71;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x9078;&#x64C7; XX &#x800C;&#x4E0D;&#x9078; XX &#x7684;&#x7406;&#x7531;</li><li>&#x4EFB;&#x52D9;&#x7C21;&#x4ECB;&#x8207;&#x57FA;&#x672C;&#x7684; Project &#x8A2D;&#x5B9A;</li><li>&#x624B;&#x52D5;&#x5316;&#x4F60;&#x7684; code signing</li><li>&#x7528; Bundler &#x7BA1;&#x7406;&#x4F60;&#x7684;&#x7CFB;&#x7D71;&#x5DE5;&#x5177;</li><li>&#x5982;&#x4F55;&#x8A2D;&#x5B9A; fastlane</li><li>&#x5982;&#x4F55;&#x8A2D;&#x5B9A; Jenkins</li></ul><p>&#x958B;&#x59CB;&#x4E4B;&#x524D;&#xFF0C;&#x4E0D;&#x662F;&#x4E00;&#x5B9A;&#xFF0C;&#x4F46;&#x6700;&#x597D;&#x5148;&#x5177;&#x5099;&#x6709;&#xFF1A;</p><ul><li>fastlane &#x7684;&#x57FA;&#x672C;&#x77E5;&#x8B58;</li><li>iOS/macOS code signing &#x7684;&#x624B;&#x52D5;&#x7BA1;&#x7406;&#x7D93;&#x9A57;</li><li>Jenkins &#x7684;&#x57FA;&#x672C;&#x77E5;&#x8B58;</li></ul><p>&#x6211;&#x5011;&#x958B;&#x59CB;&#x5427;&#xFF01;</p><h2 id="why-not-xx">Why not XX</h2><p>&#x5728;&#x9019;&#x500B;&#x4E16;&#x754C;&#x4E0A;&#xFF0C;&#x6709;&#x6210;&#x5343;&#x4E0A;&#x842C;&#x7684;&#x516C;&#x53F8;&#xFF0C;&#x904B;&#x884C;&#x8457;&#x6210;&#x5343;&#x4E0A;&#x842C;&#x7684;&#x5DE5;&#x4F5C;&#x6D41;&#x7A0B;&#xFF0C;&#x6211;&#x5011;&#x4E0D;&#x53EF;&#x80FD;&#x8A2D;&#x8A08;&#x51FA;&#x4E00;&#x5957;&#x7CFB;&#x7D71;&#xFF0C;&#x540C;&#x6642;&#x901A;&#x7528;&#x5230;&#x6240;&#x6709;&#x4EBA;&#x7684;&#x5DE5;&#x4F5C;&#x6D41;&#x7A0B;&#x4E0A;&#x3002;&#x6240;&#x4EE5;&#x8EAB;&#x70BA;&#x4E00;&#x500B;&#x7CFB;&#x7D71;&#x7684;&#x5EFA;&#x7F6E;&#x8005;&#xFF0C;&#x4F60;&#x7684;&#x4EFB;&#x52D9;&#x5C31;&#x662F;&#x8981;&#x8A2D;&#x8A08;&#x51FA;&#x4E00;&#x500B;&#x7CFB;&#x7D71;&#xFF0C;&#x8B93;&#x5B83;&#x80FD;&#x5920;&#x5B8C;&#x7F8E;&#x5730;&#x6574;&#x5408;&#x9032;&#x53BB;&#x516C;&#x53F8;&#x6216;&#x4F60;&#x500B;&#x4EBA;&#x65E2;&#x6709;&#x7684;&#x5DE5;&#x4F5C;&#x6D41;&#x7A0B;&#x4E2D;&#x3002;&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x56E0;&#x70BA;&#x7BC7;&#x5E45;&#x6709;&#x9650;&#xFF08;&#x4E5F;&#x662F;&#x56E0;&#x70BA;&#x4F5C;&#x8005;&#x6BD4;&#x8F03;&#x61F6;&#xFF09;&#xFF0C;&#x6211;&#x5011;&#x4E0D;&#x53EF;&#x80FD;&#x5217;&#x51FA;&#x6240;&#x6709;&#x53EF;&#x80FD;&#x7684;&#x8A2D;&#x8A08;&#x65B9;&#x5F0F;&#xFF0C;&#x4F46;&#x662F;&#x6211;&#x5011;&#x6703;&#x8B93;&#x4F60;&#x77E5;&#x9053;&#xFF0C;&#x6211;&#x5011;&#x6BCF;&#x4E00;&#x500B;&#x9078;&#x64C7;&#x7684;&#x539F;&#x56E0;&#xFF0C;&#x9019;&#x6A23;&#x4F60;&#x5728;&#x5BE6;&#x505A;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x5C31;&#x80FD;&#x5920;&#x77E5;&#x9053;&#x90A3;&#x4E9B;&#x6771;&#x897F;&#x662F;&#x9069;&#x5408;&#x4F60;&#x7684;&#xFF0C;&#x90A3;&#x4E9B;&#x4E0D;&#x662F;&#x3002;</p><h3 id="why-fastlane-jenkins">Why Fastlane &amp; Jenkins</h3><p>Fastlane &#x662F;&#x4E00;&#x5957;&#x71B1;&#x9580;&#x7684;CI&#x8EDF;&#x9AD4;&#xFF0C;&#x5B83;&#x63D0;&#x4F9B;&#x4E86;&#x8A31;&#x591A;&#x597D;&#x7528;&#x7684;&#x5DE5;&#x5177;&#xFF0C;&#x9084;&#x6709;&#x76F4;&#x89C0;&#x7684;&#x8173;&#x672C;&#x6A94;&#xFF0C;&#x8B93;&#x4F60;&#x53EF;&#x4EE5;&#x4E0D;&#x7528;&#x78B0;&#x89F8;&#x5230;&#x5F88;&#x591A;&#x7CFB;&#x7D71;&#x7684;&#x7D30;&#x7BC0;&#x9762;&#xFF0C;&#x4E5F;&#x80FD;&#x5920;&#x505A;&#x5230;&#x5404;&#x7A2E;&#x5BA2;&#x5236;&#x6D41;&#x7A0B;&#xFF0C;&#x800C;&#x4E14;&#x5B83;&#x540C;&#x6642;&#x652F;&#x63F4;iOS/MacOS&#x9084;&#x6709;Android&#x5E73;&#x53F0;&#x3002;&#x5927;&#x591A;&#x6578;&#x7684;&#x8A2D;&#x5B9A;&#x4F60;&#x90FD;&#x53EF;&#x4EE5;&#x900F;&#x904E; commandline &#x76F4;&#x63A5;&#x8DDF;Xcode &#x4E92;&#x52D5;&#x505A;&#x5230;&#xFF0C;&#x4F46;&#x76F8;&#x4FE1;&#x6211;&#xFF0C;&#x4F60;&#x4E0D;&#x6703;&#x60F3;&#x8981;&#x81EA;&#x5DF1;&#x5BEB;&#x90A3;&#x4E9B; script &#x7684;XD&#x3002;&#x76EE;&#x524D;&#x6709;&#x4E9B;&#x7DDA;&#x4E0A;&#x7684; CI &#x5DE5;&#x5177;&#xFF0C;&#x50CF;&#x662F; bitrise&#xFF0C;&#x8B93;&#x4F60;&#x53EF;&#x4EE5;&#x7528;&#x9EDE;&#x6309;&#x7684;&#x5C31;&#x80FD;&#x5920;&#x8A2D;&#x5B9A;&#x597D; CI&#xFF0C;&#x4F46;&#x5982;&#x679C;&#x4F60;&#x6709;&#x9810;&#x7B97;&#x4E0A;&#x7684;&#x9650;&#x5236;&#xFF0C;&#x4E26;&#x4E14;&#x5E0C;&#x671B;&#x4EFB;&#x52D9;&#x4E5F;&#x80FD;&#x5920;&#x5728;&#x81EA;&#x5DF1;&#x7684;&#x96FB;&#x8166;&#x4E0A;&#x904B;&#x4F5C;&#xFF0C;&#x90A3; fastlane &#x5C31;&#x6703;&#x662F;&#x4F60;&#x7684;&#x6700;&#x4F73;&#x9078;&#x64C7;&#x3002;</p><p>Jenkins &#x5247;&#x662F;&#x4E00;&#x500B;&#x6709;&#x7DB2;&#x9801; UI &#x7684;&#x81EA;&#x52D5;&#x5316;&#x6392;&#x7A0B;&#x5DE5;&#x5177;&#xFF0C;&#x8B93;&#x4F60;&#x53EF;&#x4EE5;&#x5229;&#x7528; GUI &#x8A2D;&#x5B9A;&#x5DE5;&#x4F5C;&#x5167;&#x5BB9;&#xFF0C;&#x50CF;&#x662F;&#x8A2D;&#x5B9A; project &#x53C3;&#x6578;&#x7B49;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x505A;&#x5230;&#x57F7;&#x884C;&#x6392;&#x7A0B;&#xFF0C;&#x50CF;&#x662F;&#x6BCF;&#x65E5;&#x5B9A;&#x6642;&#x4EFB;&#x52D9;&#x7B49;&#x3002;&#x4E5F;&#x56E0;&#x70BA;&#x6709;&#x5927;&#x91CF; plugin &#x7684;&#x95DC;&#x4FC2;&#xFF0C;&#x8DDF;&#x5F88;&#x591A;&#x65E2;&#x6709;&#x5E73;&#x53F0;&#x7684;&#x6574;&#x5408;&#x4E5F;&#x90FD;&#x4E0D;&#x932F;&#x3002;&#x5176;&#x5B83;&#x7684;&#x9078;&#x64C7;&#x4E5F;&#x6709;&#x50CF;&#x662F;&#x96F2;&#x7AEF;&#x7684; CI &#x7CFB;&#x7D71;&#x5982; BuddyBuild (&#x606D;&#x559C;&#x52A0;&#x5165; Apple )&#x3001;CircleCI &#x7B49;&#x7B49;&#x53EF;&#x4EE5;&#x4F7F;&#x7528;&#xFF0C;&#x597D;&#x8655;&#x662F;&#x8A2D;&#x5B9A;&#x66F4;&#x52A0;&#x7C21;&#x55AE;&#x4E86;&#xFF0C;&#x4F46;&#x76F8;&#x5C0D;&#x5730;&#x80FD;&#x5BA2;&#x5236;&#x7684;&#x90E8;&#x4EFD;&#x5C31;&#x6BD4;&#x8F03;&#x5C11;&#xFF0C;&#x4E26;&#x4E14;&#x56E0;&#x70BA;&#x6A5F;&#x5668;&#x4E0D;&#x5728;&#x6211;&#x5011;&#x624B;&#x908A;&#xFF0C;&#x9047;&#x5230;&#x6BD4;&#x8F03;&#x9EBB;&#x7169;&#x7684;&#x554F;&#x984C;&#x4E5F;&#x5C31;&#x53EA;&#x80FD;&#x6C42;&#x52A9; support&#x3002;&#x5C0D; Jenkins &#x4F86;&#x8AAA;&#xFF0C;&#x4F60;&#x9664;&#x4E86;&#x53EF;&#x4EE5;&#x8CB7;&#x4E00;&#x53F0; Mac mini &#x4F86;&#x67B6;&#x7AD9;&#x4E4B;&#x5916;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x67B6;&#x5728;&#x81EA;&#x5DF1;&#x7684;&#x96FB;&#x8166;&#x4E0A;&#x65B9;&#x4FBF;&#x6E2C;&#x8A66;&#xFF0C;&#x6240;&#x4EE5;&#x9019;&#x7BC7;&#x6211;&#x5011;&#x4E3B;&#x8981;&#x9084;&#x662F;&#x4EE5; Jenkins &#x70BA;&#x4E3B;&#x3002;</p><h3 id="why-not-match-or-sighcert">Why not match or sigh/cert</h3><p>&#x4F60;&#x5927;&#x6982;&#x5DF2;&#x7D93;&#x77E5;&#x9053;(&#x6216;&#x8005;&#x5DF2;&#x7D93;&#x5F88;&#x719F;&#x6089;)&#xFF0C;fastlane &#x63D0;&#x4F9B;&#x4E86;&#x8A31;&#x591A;&#x597D;&#x7528;&#x7684;&#x5DE5;&#x5177;&#xFF0C;&#x50CF;&#x662F; match &#x6216;&#x8005;&#x662F; cert/sigh&#xFF0C;&#x4F86;&#x5E6B;&#x6211;&#x5011;&#x7BA1;&#x7406; code signing&#xFF0C;&#x4F46;&#x6211;&#x5011;&#x9019;&#x7BC7;&#x4E26;&#x4E0D;&#x6703;&#x4F7F;&#x7528;&#x8005;&#x4E9B;&#x5DE5;&#x5177;&#xFF0C;&#x70BA;&#x751A;&#x9EBC;&#x5462;&#xFF1F;&#x81EA;&#x52D5;&#x5316;&#x7684;&#x5DE5;&#x5177;&#x7684;&#x78BA;&#x5F88;&#x65B9;&#x4FBF;&#xFF0C;&#x4F46;&#x5C0D;&#x65BC;&#x5927;&#x516C;&#x53F8;&#x6216;&#x662F;&#x5916;&#x5305;&#x4EBA;&#x54E1;&#x4F86;&#x8AAA;&#xFF0C;&#x9996;&#x5148;&#xFF0C;&#x4E26;&#x4E0D;&#x662F;&#x6240;&#x6709;&#x958B;&#x767C;&#x8005;&#x90FD;&#x6709; developer portal &#x7684;&#x6B0A;&#x9650;&#x7684;&#xFF0C;&#x5F88;&#x591A;&#x958B;&#x767C;&#x8005;&#x56E0;&#x70BA;&#x516C;&#x53F8;&#x653F;&#x7B56;&#x7684;&#x95DC;&#x4FC2;&#xFF0C;&#x53EA;&#x80FD;&#x53D6;&#x5F97;&#x958B;&#x767C;&#x8005;&#x7684;&#x6B0A;&#x9650;&#xFF0C;&#x8981;&#x4FEE;&#x6539;&#x6216;&#x662F;&#x4E0B;&#x8F09; certificate/provisioning profile &#x90FD;&#x662F;&#x4E0D;&#x884C;&#x7684;&#x3002;&#x9019;&#x6642;&#x5019; match/sigh/cert &#x5C31;&#x5B8C;&#x5168;&#x6D3E;&#x4E0D;&#x4E0A;&#x7528;&#x5834;&#x4E86;&#x3002;&#x9664;&#x6B64;&#x4E4B;&#x5916;&#xFF0C;&#x5927;&#x5BB6;&#x61C9;&#x8A72;&#x90FD;&#x6709;&#x8DDF; Xcode &#x88E1;&#x7684; code signing &#x596E;&#x6230;&#x7684;&#x7D93;&#x9A57;&#x5427;&#xFF1F;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x5728; code signing &#x9019;&#x908A;&#xFF0C;&#x8D8A;&#x5C11;&#x9ED1;&#x76D2;&#x5B50;&#xFF0C;&#x6D41;&#x7A0B;&#x8D8A;&#x900F;&#x660E;&#xFF0C;&#x672A;&#x4F86;&#x9047;&#x5230;&#x554F;&#x984C;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x5C31;&#x8D8A;&#x5BB9;&#x6613;&#x9032;&#x5165;&#x72C0;&#x6CC1;&#x4E26;&#x4E14;&#x627E;&#x5230;&#x89E3;&#x6C7A;&#x65B9;&#x6CD5;&#x3002;</p><h2 id="our-task">Our task</h2><p>&#x5047;&#x8A2D;&#x6211;&#x5011;&#x73FE;&#x5728;&#x6B63;&#x5728;&#x958B;&#x767C;&#x4E00;&#x500B; app &#x53EB; Brewer&#xFF0C;&#x5B83;&#x6BCF;&#x5929;&#x665A;&#x4E0A;&#x90FD;&#x6703;&#x6E96;&#x6642;&#x91C0;&#x5564;&#x9152;&#xFF0C;&#x4F60;&#x53EF;&#x4EE5;&#x5728;github&#x4E0A;&#x9762;&#x627E;&#x5230;&#x5B83;&#xFF1A;</p><p><a href="https://github.com/koromiko/Brewer">GitHub - koromiko/Brewer: I brew beer everyday :)</a></p><p>&#x6211;&#x5011;&#x7684;&#x5DE5;&#x4F5C;&#x6D41;&#x7A0B;&#x662F;&#xFF1A;</p><ol><li>&#x6BCF;&#x5929;&#x4E00;&#x65E9;&#xFF0C;&#x5DE5;&#x7A0B;&#x5E2B;&#x8981;&#x62FF;&#x5230;&#x4E00;&#x500B;&#x6700;&#x65B0;&#x7684; build&#xFF0C;&#x4F86;&#x5E6B;&#x540C;&#x4E8B;&#x505A;&#x7C21;&#x6613;&#x7684;&#x624B;&#x52D5;&#x6E2C;&#x8A66; (Staging build)</li><li>&#x6BCF;&#x661F;&#x671F;&#x7684;&#x4E00;&#x958B;&#x59CB;&#xFF0C;&#x4E5F;&#x6703;&#x7522;&#x751F;&#x4E00;&#x500B; build&#xFF0C;&#x8B93;&#x516C;&#x53F8;&#x88E1;&#x7684; QA &#x4EBA;&#x54E1;&#x505A;&#x5B8C;&#x6574;&#x7684;&#x6E2C;&#x8A66; (Production build)</li></ol><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684;CD&#x7CFB;&#x7D71;&#xFF0C;&#x8981;&#x80FD;&#x5920;</p><ol><li>&#x91DD;&#x5C0D;&#x4E0D;&#x540C;&#x7684;&#x4EFB;&#x52D9;&#x5207;&#x63DB;&#x4E0D;&#x540C;&#x7684; build configuration (staging/production)</li><li>&#x91DD;&#x5C0D;&#x4E0D;&#x540C;&#x4EFB;&#x52D9;&#x4F7F;&#x7528;&#x4E0D;&#x540C;&#x7684; code signing</li><li>&#x628A;&#x6253;&#x5305;&#x597D;&#x7684;build&#x9001;&#x5230;&#x767C;&#x4F48;&#x7CFB;&#x7D71; (Crashlytics, testFlight, etc)</li><li>&#x56FA;&#x5B9A;&#x6642;&#x9593;&#x555F;&#x52D5;&#x4EFB;&#x52D9;</li></ol><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x6703;&#x9019;&#x6A23;&#x8A2D;&#x8A08;&#x6211;&#x5011;&#x7684; CD &#x7CFB;&#x7D71;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/pic1-001.jpeg" class="kg-image" alt="pic1.001.jpeg" loading="lazy"></figure><p>&#x5728;&#x5716;&#x7684;&#x6700;&#x4E0A;&#x9762;&#xFF0C;&#x662F;&#x6211;&#x5011;&#x539F;&#x672C;&#x7684;&#x5DE5;&#x4F5C;&#x6D41;&#x7A0B;&#xFF0C;&#x5C31;&#x662F;&#x4E00;&#x822C;&#x5E38;&#x898B;&#x7684;&#x958B;&#x767C;&#x3001;&#x6E2C;&#x8A66;&#x3001;&#x767C;&#x4F48;&#x6D41;&#x7A0B;&#x3002;&#x5F9E;&#x958B;&#x767C;&#x5230;&#x6E2C;&#x8A66;&#x5927;&#x6982;&#x7684;&#x6642;&#x9593;&#x662F;&#x4E00;&#x5468;&#x3002;&#x800C;&#x4E2D;&#x9593; Delivery &#x90A3;&#x4E00;&#x5217;&#xFF0C;&#x5C31;&#x662F;&#x6211;&#x5011;&#x7684; CD &#x7B56;&#x7565;&#xFF0C;&#x5728;&#x958B;&#x767C;&#x968E;&#x6BB5;&#xFF0C;&#x6BCF;&#x4E00;&#x5929;&#x90FD;&#x6703;&#x81EA;&#x52D5;&#x91CB;&#x51FA;&#x4E00;&#x500B; nightly build &#x4F9B;&#x958B;&#x767C;&#x8005;&#x505A;&#x6E2C;&#x8A66;&#xFF0C;&#x6700;&#x5F8C;&#x5728;&#x4E00;&#x9031;&#x7684;&#x7D50;&#x5C3E;&#xFF0C;&#x5247;&#x6703;&#x91CB;&#x51FA;&#x4E00;&#x500B;&#x7248;&#x672C;&#x4F9B; QA &#x4EBA;&#x54E1;&#x505A;&#x6E2C;&#x8A66;&#x3002;&#x6700;&#x5E95;&#x4E0B;&#x7684; Env&#xFF0C;&#x6307;&#x7684;&#x662F;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x91CB;&#x51FA;&#x7684;&#x7248;&#x672C;&#xFF0C;&#x662F;&#x904B;&#x884C;&#x5728;&#x600E;&#x6A23;&#x7684;&#x74B0;&#x5883;&#x3002;&#x5C0D;&#x958B;&#x767C;&#x8005;&#x4F86;&#x8AAA;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x9803;&#x5411;&#x7528;&#x904B;&#x884C;&#x5728; Staging &#x74B0;&#x5883;&#x7684;&#x7248;&#x672C;&#x505A;&#x6E2C;&#x8A66;&#xFF0C;&#x9019;&#x6A23;&#x53EF;&#x4EE5;&#x4E00;&#x76F4;&#x958B;&#x5047;&#x5E33;&#x865F;&#x3001;&#x4E82;&#x8CB7;&#x6771;&#x897F;&#x4E5F;&#x4E0D;&#x7528;&#x64D4;&#x5FC3;&#x5F04;&#x58DE; production&#x3002;&#x800C;&#x5C0D; QA &#x4EBA;&#x54E1;&#x4F86;&#x8AAA;&#xFF0C;&#x4ED6;&#x5011;&#x5C31;&#x6703;&#x5E0C;&#x671B;&#x80FD;&#x5920;&#x4F7F;&#x7528;&#x8DDF;&#x4F7F;&#x7528;&#x8005;&#x4E00;&#x6A23;&#x7684;&#x74B0;&#x5883;&#x4F86;&#x505A;&#x6E2C;&#x8A66;&#xFF0C;&#x6240;&#x4EE5;&#x6703;&#x5728; Production &#x74B0;&#x5883;&#x5E95;&#x4E0B;&#x5EFA;&#x7F6E;&#x3002;&#x6700;&#x5F8C;&#xFF0C;&#x5728; QA &#x904E;&#x5F8C;&#x5C31;&#x662F; Release &#x968E;&#x6BB5;&#xFF0C;&#x56E0;&#x70BA; Release &#x767C;&#x751F;&#x7684;&#x983B;&#x7387;&#x76F8;&#x5C0D;&#x4E0D;&#x9AD8;&#xFF0C;&#x4E26;&#x4E14;&#x5F88;&#x6709;&#x53EF;&#x80FD;&#x6C92;&#x6709;&#x4E00;&#x500B;&#x56FA;&#x5B9A;&#x7684;&#x9031;&#x671F;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x6703;&#x5C07;&#x5B83;&#x8A2D;&#x5B9A;&#x6210;&#x624B;&#x52D5;&#x767C;&#x4F48;&#x3002;&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E2D;&#x6211;&#x5011;&#x53EA;&#x6703;&#x4ECB;&#x7D39;&#x524D;&#x5169;&#x7A2E;&#x8A2D;&#x5B9A;&#x65B9;&#x6CD5;&#xFF0C;&#x4E00;&#x65E6;&#x77E5;&#x9053;&#x8981;&#x600E;&#x6A23;&#x8A2D;&#x5B9A;&#x4E4B;&#x5F8C;&#xFF0C;&#x5F8C;&#x9762;&#x7684;&#x8A2D;&#x5B9A;&#x61C9;&#x8A72;&#x90FD;&#x53EF;&#x4EE5;&#x5F97;&#x5FC3;&#x61C9;&#x624B;&#x3002;</p><p>&#x63A5;&#x8457;&#x6211;&#x5011;&#x8981;&#x4F86;&#x4E86;&#x89E3;&#x4E00;&#x4E0B;&#xFF0C;&#x600E;&#x6A23;&#x900F;&#x904E; Xcode &#x7684; build configuration &#x505A;&#x5230;&#x74B0;&#x5883;&#x7684;&#x5207;&#x63DB;&#xFF0C;&#x800C;&#x4E0D;&#x7528;&#x66F4;&#x6539;code&#x3002;&#x8A73;&#x7D30;&#x7684;&#x5167;&#x5BB9;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x9019;&#x7BC7;&#x6587;&#x7AE0; <a href="https://medium.com/flawless-app-stories/manage-different-environments-in-your-swift-project-with-ease-659f7f3fb1a6">Manage different environments in your Swift project with ease</a> &#x7684;&#x7B2C; 3, 4 &#x9EDE;&#x3002;&#x7C21;&#x55AE;&#x5730;&#x4F86;&#x8AAA;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x900F;&#x904E; project &#x7684; build configuration &#x4F86;&#x8A2D;&#x5B9A;&#x4E0D;&#x540C;&#x7684; Flag&#xFF0C;&#x505A;&#x5230;&#x5207;&#x63DB; Staging &#x7248;&#x672C;&#x8DDF; Production &#x7684;&#x6548;&#x679C;&#xFF0C;&#x6211;&#x5011;&#x7684; build configuration &#x8A2D;&#x5B9A;&#x5982;&#x4E0B;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-13-at-23-40-17.png?w=2048" class="kg-image" alt="Screen Shot 2018-03-13 at 23.40.17" loading="lazy"></figure><p>&#x91DD;&#x5C0D;&#x4E0D;&#x540C;&#x7684;&#x8A2D;&#x5B9A;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x8A2D;&#x5B9A;&#x4E0D;&#x4E00;&#x6A23;&#x7684; AppID&#xFF0C;&#x9019;&#x6A23;&#x7684;&#x597D;&#x8655;&#x662F;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x5728;&#x6E2C;&#x8A66;&#x88DD;&#x7F6E;&#x4E0A;&#x540C;&#x6642;&#x5B89;&#x88DD;&#x4E0D;&#x540C;&#x7684;&#x7248;&#x672C;&#x4E5F;&#x4E0D;&#x7528;&#x64D4;&#x5FC3;&#x641E;&#x6DF7;&#xFF0C;&#x5C31;&#x767C;&#x4F48;&#x7CFB;&#x7D71;&#x4F86;&#x8AAA;&#xFF0C;&#x4E5F;&#x6BD4;&#x8F03;&#x597D;&#x7BA1;&#x7406;&#x3002;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-15-at-08-48-31.png?w=2048" class="kg-image" alt="Screen Shot 2018-03-15 at 08.48.31" loading="lazy"></figure><p>&#x63A5;&#x8457;&#x6211;&#x5011;&#x8981;&#x4F86;&#x91DD;&#x5C0D;&#x4E0D;&#x540C;&#x7684; Configuration &#x505A;&#x4E0D;&#x540C;&#x7684; code signing&#x3002;</p><h2 id="%E8%AE%93-certificate-%E8%B7%9F-provisioning-profile-%E5%9B%9E%E6%AD%B8%E4%BD%A0%E7%9A%84%E6%8E%A7%E5%88%B6">&#x8B93; certificate &#x8DDF; provisioning profile &#x56DE;&#x6B78;&#x4F60;&#x7684;&#x63A7;&#x5236;</h2><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-05-04-at-16-01-04.png" class="kg-image" alt="Screen Shot 2018-05-04 at 16.01.04.png" loading="lazy"></figure><p>&#x5BEB; iOS/macOS &#x7684;&#x958B;&#x767C;&#x8005;&#xFF0C;&#x61C9;&#x8A72;&#x5DF2;&#x7D93;&#x975E;&#x5E38;&#x719F;&#x6089;&#x9019;&#x500B;&#x756B;&#x9762;&#x4E86;&#x5427;&#xFF01;&#x6C92;&#x932F;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x7B2C;&#x4E00;&#x6642;&#x9593;&#x628A;&#x81EA;&#x52D5;&#x7BA1;&#x7406;&#x6191;&#x8B49;&#x95DC;&#x6389;&#xFF0C;&#x4E0D;&#x7136;&#x4F60;&#x53EF;&#x80FD;&#x6703;&#x9032;&#x5165;&#x81EA;&#x52D5;&#x66F4;&#x65B0;&#x7684;&#x7121;&#x9593;&#x5730;&#x7344;&#x3002;&#x628A;&#x81EA;&#x52D5;&#x5316;&#x95DC;&#x6389;&#x4E4B;&#x5F8C;&#xFF0C;&#x5C31;&#x8981;&#x4F86;&#x6E96;&#x5099;&#x597D;&#x6240;&#x6709; code signing &#x6240;&#x9700;&#x8981;&#x7684;&#x6A94;&#x6848;&#x4E86;&#xFF0C;&#x91DD;&#x5C0D;&#x6211;&#x5011;&#x4E0A;&#x9762;&#x7684;&#x74B0;&#x5883;&#x8A2D;&#x5B9A;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x6E96;&#x5099;&#x9019;&#x4E9B;&#x6A94;&#x6848;&#xFF1A;</p><ul><li><strong>Provisioning profile</strong>: &#x91DD;&#x5C0D;&#x4E0D;&#x540C; AppID &#x7684; Ad Hoc distribution profile</li><li><strong>Certificate</strong>: Ad Hoc distribution certificate&#xFF0C;&#x4E26;&#x4E14;&#x532F;&#x51FA;&#x6210; <code>.p12</code></li></ul><p>&#x8981;&#x6CE8;&#x610F;&#x7684;&#x662F;&#xFF0C;&#x5728; developer portal &#x4E0B;&#x8F09;&#x7684;&#x6191;&#x8B49;&#x6A94;&#x662F;&#x5229;&#x7528; DEM &#x52A0;&#x5BC6;&#x7684; <code>.cer</code> &#x6A94;&#xFF0C;&#x4F46; DEM &#x7684;&#x6A94;&#x6848;&#x88E1;&#x4E26;&#x6C92;&#x6709;&#x5305;&#x62EC;&#x79C1;&#x9470;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#x5982;&#x679C;&#x4F60;&#x63DB;&#x96FB;&#x8166;&#x4E86;&#xFF0C;&#x9019;&#x5F35;&#x6191;&#x8B49;&#x5C31;&#x6703;&#x56E0;&#x70BA;&#x627E;&#x4E0D;&#x5230;&#x4F60;&#x7684;&#x79C1;&#x9470;&#x800C;&#x5931;&#x6548;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5FC5;&#x9700;&#x8981;&#x628A; DEM &#x8F38;&#x51FA;&#x6210;&#x5305;&#x542B;&#x79C1;&#x9470;&#x7684; P12 &#x6A94;&#xFF0C;&#x8F38;&#x51FA;&#x7684;&#x65B9;&#x6CD5;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x9019;&#x7BC7; <a href="https://stackoverflow.com/questions/39091048/convert-cer-to-p12">stackoverfow</a>&#x3002;</p><p>&#x6700;&#x5F8C;&#x6211;&#x5011;&#x628A;&#x4E0A;&#x8FF0;&#x7684;&#x6A94;&#x6848;&#x90FD;&#x4E0B;&#x8F09;&#x4E0B;&#x4F86;&#xFF0C;&#x5B58;&#x5230;&#x53E6;&#x5916;&#x958B;&#x7684; codesigning &#x8CC7;&#x6599;&#x593E;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-14-at-08-29-55.png" class="kg-image" alt="Screen Shot 2018-03-14 at 08.29.55.png" loading="lazy"></figure><p>&#x9019;&#x500B;&#x8CC7;&#x6599;&#x593E;<strong>&#x4E0D;&#x80FD;</strong>&#x8DDF;&#x4F60;&#x7684; code &#x653E;&#x5728;&#x4E00;&#x8D77;&#xFF0C;&#x5B83;&#x53EF;&#x4EE5;&#x653E;&#x5728; jenkins &#x4E3B;&#x6A5F;&#x4E0A;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x505A;&#x6210;&#x4E00;&#x500B; git repository &#x6216;&#x8005;&#x4E00;&#x500B;&#x96F2;&#x7AEF;&#x5171;&#x4EAB;&#x8CC7;&#x6599;&#x593E;&#xFF0C;&#x518D;&#x5229;&#x7528; fastlane &#x5C07;&#x9019;&#x4E9B;&#x6A94;&#x6848;&#x4E0B;&#x8F09;&#x4E0B;&#x4F86;&#x4F7F;&#x7528;&#xFF0C;&#x597D;&#x8655;&#x662F;&#x4E0D;&#x7528;&#x6BCF;&#x6B21;&#x79FB;&#x6A5F;&#x6642;&#x4E5F;&#x8981;&#x624B;&#x52D5;&#x79FB;&#x52D5;&#x9019;&#x4E9B;&#x6A94;&#x6848;&#x3002;&#x653E;&#x9060;&#x7AEF;&#x7684;&#x4F5C;&#x6CD5;&#x57FA;&#x672C;&#x4E0A;&#x662F;&#x5B89;&#x5168;&#x7684;&#xFF0C;&#x5982;&#x679C;&#x4F60;&#x89BA;&#x5F97;&#x653E;&#x9060;&#x7AEF;&#x5B89;&#x5168;&#x6027;&#x662F;&#x6709;&#x7591;&#x616E;&#x7684;&#xFF0C;&#x53EF;&#x4EE5;&#x53C3;&#x8003; <a href="https://docs.fastlane.tools/actions/match/#is-this-secure">match - fastlane docs</a>&#x3002;</p><h2 id="%E7%94%A8-bundler-%E7%AE%A1%E7%90%86%E4%BD%A0%E7%9A%84%E7%B3%BB%E7%B5%B1%E5%B7%A5%E5%85%B7">&#x7528; Bundler &#x7BA1;&#x7406;&#x4F60;&#x7684;&#x7CFB;&#x7D71;&#x5DE5;&#x5177;</h2><p>&#x5982;&#x679C;&#x4F60;&#x662F; iOS/macOS &#x958B;&#x767C;&#x8005;&#xFF0C;&#x4F60;&#x61C9;&#x8A72;&#x5DF2;&#x7D93;&#x5F88;&#x719F;&#x6089;&#x7528; <a href="https://brew.sh/">homebrew</a> &#x4F86;&#x5B89;&#x88DD;&#x50CF;&#x662F; Cocoapods &#x6216;&#x662F; Carthage &#x7B49;&#x7B49;&#x5957;&#x4EF6;&#xFF0C;homebrew &#x8B93;&#x4F60;&#x53EF;&#x4EE5;&#x8F15;&#x6613;&#x5730;&#x7BA1;&#x7406;&#x4F60;&#x7684;&#x7CFB;&#x7D71;&#x7A0B;&#x5F0F;&#xFF0C;&#x8B93;&#x4F60;&#x81EA;&#x5DF1;&#x96FB;&#x8166;&#x88E1;&#x9762;&#x7684;&#x74B0;&#x5883;&#x4FDD;&#x6301;&#x4E00;&#x81F4;&#x3002;&#x4F46;&#x662F;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x6211;&#x5011;&#x7684;&#x5EFA;&#x7F6E;&#x74B0;&#x5883;&#x80FD;&#x5920;&#x8D8A;&#x7368;&#x7ACB;&#x8D8A;&#x597D;&#xFF0C;&#x9019;&#x6A23;&#x624D;&#x4E0D;&#x6703;&#x56E0;&#x70BA;&#x63DB;&#x4E86;&#x96FB;&#x8166;&#x5EFA;&#x7F6E;&#x5C31;&#x6703;&#x51FA;&#x932F;&#xFF0C;&#x6216;&#x8005;&#x662F;&#x9084;&#x8981;&#x518D;&#x91CD;&#x8986;&#x5957;&#x4EF6;&#x5B89;&#x88DD;&#x4E00;&#x6A23;&#x7684;&#x52D5;&#x4F5C;&#x3002;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x53EF;&#x4EE5;&#x88FD;&#x4F5C;&#x51FA;&#x4E00;&#x500B;&#x7368;&#x7ACB;&#x7684;&#x74B0;&#x5883;&#xFF0C;&#x53EF;&#x4EE5;&#x88AB;&#x5B8C;&#x6574;&#x5730;&#x8907;&#x88FD;&#x5230;&#x4E0D;&#x540C;&#x96FB;&#x8166;&#x4E0A;&#xFF0C;&#x9019;&#x6642;&#x5019;&#x6211;&#x5011;&#x5C31;&#x6703;&#x9700;&#x8981; <a href="http://bundler.io/">Bundler</a>&#x3002;Bundler &#x662F;&#x4E00;&#x500B; Ruby &#x7684;&#x74B0;&#x5883;&#x7BA1;&#x7406;&#x7CFB;&#x7D71;&#xFF0C;&#x5B83;&#x53EF;&#x4EE5;&#x5E6B;&#x4F60;&#x8A2D;&#x5B9A;&#x51FA;&#x4E00;&#x500B;&#x80FD;&#x5920;&#x88AB;&#x8907;&#x88FD;&#x7684;&#x865B;&#x64EC;&#x74B0;&#x5883;&#xFF0C;&#x4E4B;&#x5F8C;&#x4E0D;&#x7BA1;&#x63DB;&#x5230;&#x4EFB;&#x4F55;&#x96FB;&#x8166;&#xFF0C;&#x5C0D;&#x5728;&#x9019;&#x500B;&#x865B;&#x64EC;&#x74B0;&#x5883;&#x4E2D;&#x7684;&#x7A0B;&#x5F0F;&#x4F86;&#x8AAA;&#xFF0C;&#x57F7;&#x884C;&#x7684;&#x74B0;&#x5883;&#x90FD;&#x662F;&#x4E00;&#x6A21;&#x4E00;&#x6A23;&#x7684;&#x3002;</p><p>&#x6211;&#x5011;&#x6703;&#x900F;&#x904E; bundler &#x7BA1;&#x7406; fastlane &#x8DDF; Cocoapods &#x7684;&#x5B89;&#x88DD;&#xFF0C;&#x6240;&#x4EE5;&#x8ACB;&#x5728; project folder &#x88E1;&#x9762;&#x65B0;&#x589E;&#x4E00;&#x500B; <code>Gemfile</code><strong> </strong>&#x6A94;&#x6848;&#xFF0C;&#x5167;&#x5BB9;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-rudy">source &quot;https://rubygems.org&quot;

gem &quot;fastlane&quot;
gem &quot;cocoapods&quot;</code></pre><p><code>Gemfile</code> &#x662F; Ruby &#x5957;&#x4EF6;&#x7BA1;&#x7406;&#x7A0B;&#x5F0F;gem&#x7684;&#x8A2D;&#x5B9A;&#x6A94;&#xFF0C;&#x88E1;&#x9762;&#x63CF;&#x8FF0;&#x4E86;&#x4F60;&#x9019;&#x500B;project&#x6240;&#x9700;&#x8981;&#x7528;&#x5230;&#x7684;&#x7A0B;&#x5F0F;&#x3002;Bundler&#x6703;&#x900F;&#x904E;gem&#x4F86;&#x5B89;&#x88DD;&#x7A0B;&#x5F0F;&#xFF0C;&#x4E26;&#x4E14;&#x5E6B;&#x4F60;&#x8A2D;&#x5B9A;&#x597D;&#x865B;&#x64EC;&#x74B0;&#x5883;&#x3002;</p><p>&#x63A5;&#x8457;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x4F86;&#x5B89;&#x88DD;&#x9019;&#x5169;&#x96BB;&#x7A0B;&#x5F0F;&#xFF1A;</p><pre><code>bundle install --path vendor/bundler</code></pre><p><code>&#x2014;path vendor/bundler</code> &#x8868;&#x793A;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x628A;&#x7A0B;&#x5F0F;&#x5B89;&#x88DD;&#x5728;&#x7576;&#x524D;&#x7684;&#x76EE;&#x9304;&#x5E95;&#x4E0B;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x88DD;&#x5230;&#x7CFB;&#x7D71;&#x6A94;&#x6848;&#x593E;&#x5982; <code>/usr/local/bin</code> &#x6216; <code>usr/bin</code>&#x88E1;&#x9762;&#xFF0C;&#x9019;&#x6A23;&#x4F60;&#x7684; project &#x5C31;&#x4E0D;&#x6703;&#x8DDF;&#x4F60;&#x7684;&#x7CFB;&#x7D71;&#x6709;&#x4EFB;&#x4F55;&#x639B;&#x52FE;&#x4E86;&#x3002;</p><p><em>&#x8ACB;&#x8A18;&#x5F97;&#x5C07; <code>vendor</code> &#x9019;&#x500B;&#x8CC7;&#x6599;&#x593E;&#x52A0;&#x5165; <code>.gitignore</code> &#x4E4B;&#x4E2D;&#x3002;</em></p><p>&#x8A2D;&#x5B9A;&#x5B8C; bundler &#x4E4B;&#x5F8C;&#xFF0C;&#x672A;&#x4F86;&#x6211;&#x5011;&#x60F3;&#x8981;&#x5728;&#x865B;&#x64EC;&#x74B0;&#x5883;&#x4E0B;&#x57F7;&#x884C;&#x7A0B;&#x5F0F;&#x7684;&#x8A71;&#xFF0C;&#x5C31;&#x9700;&#x8981;&#x4E0B; bundler exec &#x7684;&#x524D;&#x7DB4;&#xFF0C;&#x6BD4;&#x65B9;&#x8AAA;&#xFF0C;fastlane init &#x662F;&#x521D;&#x59CB;&#x5316;&#x4E00;&#x500B; fastlane &#x7684; project&#xFF0C;&#x73FE;&#x5728;&#x6211;&#x5011;&#x8981;&#x6539;&#x6210; <code>bundler exec fastlane init</code>&#xFF0C;&#x8868;&#x793A;&#x6211;&#x5011;&#x8981;&#x521D;&#x59CB;&#x5316;&#x4E00;&#x500B; fastlane project&#xFF0C;&#x4E26;&#x4E14;&#x5728;&#x525B;&#x525B;&#x8A2D;&#x5B9A;&#x7684;&#x865B;&#x64EC;&#x74B0;&#x5883;&#x4E0B;&#x57F7;&#x884C;&#x9019;&#x500B;&#x6307;&#x4EE4;&#x3002;</p><h2 id="%E4%BE%86fastlane%E4%B8%80%E4%B8%8B">&#x4F86;fastlane&#x4E00;&#x4E0B;</h2><p><a href="https://docs.fastlane.tools/">fastlane docs</a> &#x662F;&#x4E00;&#x500B;&#x597D;&#x7528;&#x7684;&#x5DE5;&#x5177;&#xFF0C;&#x5E6B;&#x52A9;&#x4F60;&#x81EA;&#x52D5;&#x5316;&#x5B8C;&#x6210;&#x8A31;&#x591A;&#x7E41;&#x8907;&#x7684;&#x5DE5;&#x4F5C;&#x3002;&#x5C31;&#x7B97;&#x4F60;&#x662F;&#x4E00;&#x4EBA;&#x5718;&#x968A;&#xFF0C;&#x4F60;&#x4E5F;&#x53EF;&#x4EE5;&#x900F;&#x904E; fastlane &#x81EA;&#x52D5;&#x5316;&#x6240;&#x6709;&#x6E2C;&#x8A66;&#x3001;&#x767C;&#x4F48;&#x8DDF;&#x6191;&#x8B49;&#x7BA1;&#x7406;&#x7B49;&#x7B49;&#x5DE5;&#x4F5C;&#x3002;</p><p>&#x6211;&#x5011;&#x6253;&#x7B97;&#x5229;&#x7528; fastlane&#xFF0C;&#x52A0;&#x5165;&#x5169;&#x500B;&#x4EFB;&#x52D9;&#xFF0C;&#x4E00;&#x500B;&#x662F; staging &#x74B0;&#x5883;&#x7684;&#x767C;&#x4F48;&#xFF0C;&#x4E00;&#x500B;&#x662F; production &#x74B0;&#x5883;&#x7684;&#x767C;&#x4F48;&#x3002;&#x9019;&#x5169;&#x7A2E;&#x8A2D;&#x5B9A;&#x90FD;&#x5305;&#x542B;&#x4EE5;&#x4E0B;&#x56FA;&#x5B9A;&#x7684;&#x5DE5;&#x4F5C;&#xFF1A;</p><ul><li>&#x5F9E;&#x6A94;&#x6848;&#x532F;&#x5165; certificate &#x8207; provisioning profile</li><li>build app &#x4E26;&#x4E14;&#x4E0A;&#x50B3;&#x5230; Crashlytics</li></ul><p>&#x5169;&#x7A2E;&#x8A2D;&#x5B9A;&#x5DEE;&#x5225;&#x53EA;&#x5728;&#x65BC; build configuration &#x7684;&#x4E0D;&#x540C;&#x800C;&#x5DF2;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684; fastlane &#x8981;&#x80FD;&#x4F9D;&#x64DA;&#x6211;&#x5011;&#x6240;&#x9078;&#x53D6;&#x7684;&#x4EFB;&#x52D9;&#xFF0C;&#x627E;&#x5230;&#x5C0D;&#x61C9;&#x7684; certificate &#x8DDF; provisioning profile&#xFF0C;&#x7528;&#x5B83;&#x5011;&#x4F86;&#x5EFA;&#x7F6E; app &#x4E26;&#x767C;&#x4F48;&#x3002;</p><p>&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x5C07;&#x4EE5; Swift &#x8A2D;&#x5B9A;&#x6A94;&#x4F86;&#x7576;&#x7BC4;&#x4F8B;&#x3002;&#x9078; Swift &#x4F86;&#x64B0;&#x5BEB;&#x8A2D;&#x5B9A;&#x6A94;&#xFF0C;&#x76EE;&#x524D; fastlane Swift &#x662F;&#x57FA;&#x65BC; Ruby &#x7684; wrapping&#xFF0C;&#x4E26;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x539F;&#x751F; Swift&#xFF0C;&#x6240;&#x4EE5; fastlane plugin &#x662F;&#x4E0D;&#x88AB;&#x652F;&#x63F4;&#x7684;&#xFF0C;&#x6709;&#x9700;&#x8981;&#x7528; plugin &#x7684;&#x9019;&#x9EDE;&#x53EF;&#x80FD;&#x8981;&#x8003;&#x616E;&#x4E00;&#x4E0B;&#x3002;&#x53E6;&#x5916;&#xFF0C;Swift &#x4E5F;&#x7121;&#x6CD5; catch &#x4EFB;&#x4F55;Ruby &#x767C;&#x51FA;&#x4F86;&#x7684;&#x4F8B;&#x5916;&#xFF0C;&#x5982;&#x679C;&#x6709;&#x9700;&#x8981;&#x5B8C;&#x6574;&#x7684;&#x4F8B;&#x5916;&#x8655;&#x7406;&#xFF0C;&#x5C31;&#x9084;&#x662F;&#x8ACB;&#x7528; Ruby &#x539F;&#x751F;&#x7684; fastlane &#x5427;&#x3002;</p><p>&#x5B89;&#x88DD;&#x65B9;&#x6CD5;&#x8ACB;&#x53C3;&#x8003; <a href="https://docs.fastlane.tools/getting-started/ios/setup/">Setup - fastlane docs</a>&#x3002;</p><p>&#x5B89;&#x88DD;&#x5B8C;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x5148;&#x521D;&#x59CB;&#x5316;&#x6211;&#x5011;&#x7684; fastlane project&#xFF1A;</p><pre><code>bundler exec fastlane init swift</code></pre><p>&#x63A5;&#x8457;&#xFF0C;&#x4F60;&#x5C31;&#x53EF;&#x4EE5;&#x5728;<code>fastlane/Fastfile.swift</code>&#x627E;&#x5230;&#x4F60;&#x7684; fastlane &#x8A2D;&#x5B9A;&#x6A94;&#x3002;&#x5728;&#x8A2D;&#x5B9A;&#x6A94;&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x627E;&#x5230; <code>Fastfile</code> &#x9019;&#x500B; class&#xFF0C;&#x9019;&#x500B; class &#x88E1;&#x9762;&#x6240;&#x5B9A;&#x7FA9;&#x7684; method&#xFF0C;&#x53EA;&#x8981;&#x662F;&#x4EE5; Lane &#x7D50;&#x5C3E;&#x7684; method&#xFF0C;&#x90FD;&#x6703;&#x88AB;&#x8A8D;&#x5B9A;&#x70BA;&#x662F;&#x4E00;&#x500B; &quot;lane&quot;&#xFF0C;&#x53EF;&#x4EE5;&#x900F;&#x904E; fastlane &#x4F86;&#x57F7;&#x884C;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5148;&#x65B0;&#x589E;&#x5169;&#x500B;lane&#xFF0C;&#x4E00;&#x500B;&#x53EB;&#x505A; <code>developerRelease</code>&#xFF0C;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x53EB;&#x505A; <code>qaRelase</code>&#xFF1A;</p><pre><code class="language-swift">class Fastfile: LaneFile {
    func developerReleaseLane() {
        desc(&quot;Create a developer release&quot;)
	package(config: Staging())
	crashlytics
    }

    func qaReleaseLane() {
        desc(&quot;Create a qa release&quot;)
        package(config: Production())
        crashlytics
    }
}</code></pre><p>&#x672A;&#x4F86;&#x6211;&#x5011;&#x60F3;&#x8981;&#x6253;&#x5305;release&#x7684;&#x6642;&#x5019;&#xFF0C;&#x90FD;&#x53EF;&#x4EE5;&#x57F7;&#x884C;&#x4E0B;&#x9762;&#x9019;&#x6A23;&#x7684;&#x6307;&#x4EE4;&#xFF1A;</p><pre><code class="language-shell">bundle exec fastlane developerRelease
# or
bundle exec fastlane qaRelease </code></pre><p>&#x60F3;&#x8981;&#x6253;&#x5305;&#x7D66; developer &#x5C31;&#x57F7;&#x884C;&#x4E0A;&#x9762;&#x7684;&#x6307;&#x4EE4;&#xFF0C;&#x60F3;&#x8981;&#x6253;&#x5305;&#x7D66; QA &#x4EBA;&#x54E1;&#xFF0C;&#x5C31;&#x57F7;&#x884C;&#x4E0B;&#x9762;&#x7684;&#x6307;&#x4EE4;&#xFF0C;&#x9019;&#x6A23;&#x662F;&#x4E0D;&#x662F;&#x975E;&#x5E38;&#x5730;&#x76F4;&#x89C0;&#x963F;&#xFF01;(&#x662F;)&#x3002;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x770B;&#x5230;&#x5728;&#x9019;&#x5169;&#x500B; lane &#x88E1;&#x9762;&#x90FD;&#x6703;&#x547C;&#x53EB;&#x4E00;&#x500B; method&#xFF1A;<code>package</code>&#xFF0C;&#x4E26;&#x4E14;&#x8DDF;&#x64DA;&#x4E0D;&#x540C;&#x7684; lane &#x7D66;&#x4E88;&#x4E0D;&#x540C;&#x7684; config &#x53C3;&#x6578;&#xFF0C;&#x9019;&#x500B; package &#x7684;&#x63A5;&#x53E3;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-swift">func package(config: Configuration) {
}</code></pre><p>&#x5B83;&#x7684;&#x53C3;&#x6578;&#xFF0C;&#x5176;&#x5BE6;&#x662F;&#x4E00;&#x500B;&#x53EB; Configuration &#x7684; protocol&#xFF1A;</p><pre><code class="language-swift">protocol Configuration {
    /// file name of the certificate 
    var certificate: String { get } 
    
    /// file name of the provisioning profile
    var provisioningProfile: String { get } 
    
    /// configuration name in xcode project
    var buildConfiguration: String { get }
    
    /// the app id for this configuration
    var appIdentifier: String { get }
    
    /// export methods, such as &quot;ad-doc&quot; or &quot;appstore&quot;
    var exportMethod: String { get }
}</code></pre><p>&#x6240;&#x6709;&#x7684; configuration &#x90FD;&#x8981; conform &#x9019;&#x500B; protocol&#xFF0C;&#x624D;&#x80FD;&#x88AB;&#x50B3;&#x5165; package &#x88E1;&#x9762;&#x505A;&#x6253;&#x5305;&#x3002;&#x5728;&#x9019;&#x500B;&#x7BC4;&#x4F8B;&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x8A2D;&#x5B9A;&#x4E86;&#x5169;&#x7A2E; configuration&#xFF1A;</p><pre><code class="language-swift">struct Staging: Configuration { 
	var certificate = &quot;ios_distribution&quot;
	var provisioningProfile = &quot;Brewer_Staging&quot;
	var buildConfiguration = &quot;Staging&quot;
	var appIdentifier = &quot;works.sth.brewer.staging&quot;
	var exportMethod = &quot;ad-hoc&quot;
}

struct Production: Configuration { 
	var certificate = &quot;ios_distribution&quot;
	var provisioningProfile = &quot;Brewer_Production&quot;
	var buildConfiguration = &quot;Production&quot;
	var appIdentifier = &quot;works.sth.brewer.production&quot;
	var exportMethod = &quot;ad-hoc&quot;
}</code></pre><p>&#x8DDF;&#x64DA;&#x5BE6;&#x969B;&#x7684;&#x72C0;&#x6CC1;&#xFF0C;&#x5728;&#x9019;&#x5169;&#x500B;struct&#x88E1;&#x9762;&#x5BE6;&#x4F5C; protocol&#xFF0C;&#x9019;&#x6A23;&#x53EF;&#x4EE5;&#x78BA;&#x4FDD;&#x6211;&#x5011;&#x7684; package &#x6709;&#x4E00;&#x81F4;&#x7684;&#x63A5;&#x53E3;&#xFF0C;&#x540C;&#x6642;&#x53C8;&#x80FD;&#x7B26;&#x5408;&#x5404;&#x7A2E;&#x4E0D;&#x540C;&#x7684;&#x72C0;&#x6CC1;&#x3002;</p><p>&#x63A5;&#x8457;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x958B;&#x59CB;&#x4F86;&#x5BE6;&#x4F5C; package &#x4E86;&#x3002;&#x9084;&#x8A18;&#x5F97; package &#x7684;&#x4EFB;&#x52D9;&#x55CE;&#xFF1F;&#x9996;&#x5148;&#xFF0C;&#x5B83;&#x9700;&#x8981;&#x5F9E;&#x6A94;&#x6848;&#x5F15;&#x5165; certificate &#x8DDF; provisioning profile&#x3002;&#x95DC;&#x65BC; certificate&#xFF0C;&#x6211;&#x5011;&#x6703;&#x4F7F;&#x7528; <a href="https://docs.fastlane.tools/actions/import_certificate/">importCertificate</a> &#x9019;&#x500B;action&#xFF0C;&#x4F86;&#x8B80;&#x53D6;&#x7CFB;&#x7D71;&#x4E2D;&#x7684; <code>.p12</code> &#x6A94;&#xFF1A;</p><pre><code class="language-swift">importCertificate(
keychainName: environmentVariable(get: &quot;KEYCHAIN_NAME&quot;),
keychainPassword: environmentVariable(get: &quot;KEYCHAIN_PASSWORD&quot;),
certificatePath: &quot;\(ProjectSetting.codeSigningPath)/\(config.certificate).p12&quot;,
certificatePassword: ProjectSetting.certificatePassword
)</code></pre><p>KeychainName &#x9019;&#x500B;&#x53C3;&#x6578;&#x5C31;&#x586B;&#x5165;&#x4F60; keyChain &#x7684;&#x540D;&#x7A31;&#xFF0C;keyChainPassword &#x901A;&#x5E38;&#x5C31;&#x662F;&#x4F60;&#x7CFB;&#x7D71;&#x7684;&#x5BC6;&#x78BC;&#x3002;</p><p>&#x56E0;&#x70BA; fastlane &#x7684;&#x8A2D;&#x5B9A;&#x6A94;&#xFF0C;&#x901A;&#x5E38;&#x90FD;&#x6703;&#x8DDF;&#x8457;&#x539F;&#x59CB;&#x6A94;&#x4E00;&#x8D77;&#x88AB; commit &#x5230; git &#x88E1;&#xFF0C;&#x628A;&#x5BC6;&#x78BC;&#x653E;&#x5728; code &#x88E1;&#x9762;&#xFF0C;&#x901A;&#x5E38;&#x4E0D;&#x662F;&#x4E00;&#x500B;&#x597D;&#x4E3B;&#x610F;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x6703;&#x7528;&#x7CFB;&#x7D71;&#x8B8A;&#x6578;&#x4F86;&#x53D6;&#x4EE3;&#x56FA;&#x5B9A;&#x7684;&#x5B57;&#x4E32;&#x3002;&#x5728;&#x7CFB;&#x7D71;&#x4E0A;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x5B58;&#x5165;&#x7CFB;&#x7D71;&#x8B8A;&#x6578;&#xFF1A;</p><pre><code class="language-shell">export KEYCHAIN_NAME=&quot;KEYCHAIN_NAME&quot;;
export KEYCHAIN_PASSWORD=&quot;YOUR_PASSWORD&quot;;</code></pre><p>&#x800C;&#x5728; fastlane &#x88E1;&#x9762;&#xFF0C;&#x5C31;&#x5229;&#x7528; <code>environmentVariable(get:)</code> &#x4F86;&#x8B80;&#x53D6;&#x7CFB;&#x7D71;&#x8B8A;&#x6578;&#x3002;&#x9019;&#x6A23;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x907F;&#x514D;&#x628A;&#x6A5F;&#x5BC6;&#x5B57;&#x4E32; commit &#x5230; git &#x4E0A;&#xFF0C;&#x5927;&#x5927;&#x589E;&#x52A0;&#x7CFB;&#x7D71;&#x7684;&#x5B89;&#x5168;&#x6027;&#x3002; &#x63A5;&#x8457;&#x6211;&#x5011;&#x900F;&#x904E; certificatePath &#x6307;&#x5B9A; certificate <code>.p12</code> &#x6A94;&#x7684;&#x4F4D;&#x7F6E;&#x9084;&#x6709;&#x9019;&#x500B;&#x6A94;&#x6848;&#x7684;&#x5BC6;&#x78BC;&#x3002;&#x5728;&#x9019;&#x88E1;&#xFF0C; ProjectSetting &#x662F;&#x4E00;&#x500B; enum&#xFF0C;&#x5B58;&#x653E;&#x8457; project &#x76F8;&#x95DC;&#x7684;&#x8B8A;&#x6578;&#xFF0C;&#x6211;&#x5011;&#x5B9A;&#x7FA9;&#x4E86; codesigningPath &#x8207; certificatePassword&#xFF0C;&#x4EE3;&#x8868;&#x6240;&#x6709; code signing &#x7684;&#x76F8;&#x95DC;&#x6A94;&#x6848;&#x7684;&#x4F4D;&#x7F6E;&#x8DDF;&#x5BC6;&#x78BC;&#xFF0C;&#x800C;&#x9019;&#x4E9B;&#x8CC7;&#x8A0A;&#x4E00;&#x6A23;&#x662F;&#x900F;&#x904E;&#x7CFB;&#x7D71;&#x8B8A;&#x6578;&#x7D66;&#x4E88;&#x7684;&#x3002;</p><pre><code class="language-swift">enum ProjectSetting {
	static let codeSigningPath = environmentVariable(get: &quot;CODESIGNING_PATH&quot;)
	static let certificatePassword = environmentVariable(get: &quot;CERTIFICATE_PASSWORD&quot;)
}</code></pre><p>&#x4EE5;&#x4E0A; certificate &#x7684;&#x5F15;&#x5165;&#x5C31;&#x8A2D;&#x5B9A;&#x5B8C;&#x7562;&#x4E86;&#xFF01;&#x63A5;&#x4E0B;&#x4F86;&#x662F; provisioning profile &#x7684;&#x8A2D;&#x5B9A;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x5229;&#x7528; <a href="https://docs.fastlane.tools/actions/update_project_provisioning/">updateProjectProvisioning</a>&#xFF0C;&#x8DDF;&#x64DA;&#x4E0D;&#x540C;&#x7684;&#x8A2D;&#x5B9A;&#x6A94;&#xFF0C;&#x4F86;&#x66F4;&#x65B0; project &#x7684; provisioning profile&#xFF1A;</p><pre><code class="language-swift">updateProjectProvisioning(
xcodeproj: ProjectSetting.project,
profile: &quot;\(ProjectSetting.codeSigningPath)/\(config.provisioningProfile).mobileprovision&quot;,
targetFilter: &quot;^\(ProjectSetting.target)$&quot;,
buildConfiguration: config.buildConfiguration
)</code></pre><p><code>config</code> &#x5C31;&#x662F;&#x6211;&#x5011;&#x4E00;&#x958B;&#x59CB;&#x4EE3;&#x5165;&#x7684; package &#x7684;&#x53C3;&#x6578;&#xFF0C;&#x662F;&#x4E00;&#x500B; conforms Configuration protocol &#x7684; struct &#x6216; class&#x3002;&#x5728; <code>profile</code> &#x9019;&#x500B;&#x53C3;&#x6578;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x5229;&#x7528; codeSigningPath &#x8DDF;`config.provisioningProfile` &#x4F86;&#x6307;&#x5B9A; provisioning profile &#x7684;&#x4F4D;&#x7F6E;&#xFF0C;&#x6211;&#x5011;&#x540C;&#x6642;&#x4E5F;&#x5728; <code>buildConfiguration</code> &#x9019;&#x500B;&#x53C3;&#x6578;&#x88E1;&#x6307;&#x5B9A;&#x6211;&#x5011;&#x8981;&#x4FEE;&#x6539;&#x7684; build configuration&#xFF0C;&#x9019;&#x6A23; updateProjectProvisioning &#x5C31;&#x6703;&#x628A;&#x6307;&#x5B9A;&#x7684; provisioning profile &#x5BEB;&#x5165;&#x6307;&#x5B9A;&#x7684; configuration &#x88E1;&#x9762;&#x4E86;&#x3002; </p><blockquote>&#x6CE8;&#x610F;&#xFF1A;&#x9019;&#x500B; action &#x6703;&#x76F4;&#x63A5;&#x4FEE;&#x6539;&#x4F60;&#x7684; .xcodeproj&#xFF0C;&#x5C0D; Jenkis &#x4F86;&#x8AAA;&#xFF0C;&#x6240;&#x6709;&#x4FEE;&#x6539;&#x90FD;&#x662F;&#x4E0D;&#x6703;&#x88AB; commit &#x7684;&#xFF0C;&#x6240;&#x4EE5;&#x5728; CD &#x7CFB;&#x7D71;&#x4E2D;&#x9019;&#x4E9B;&#x4FEE;&#x6539;&#x90FD;&#x662F;&#x6C92;&#x6709;&#x554F;&#x984C;&#x7684;&#xFF0C;&#x4F46;&#x5982;&#x679C;&#x4F60;&#x5E0C;&#x671B;&#x5728;&#x4F60;&#x7684; working directory &#x88E1;&#x9762;&#x57F7;&#x884C; fastlane&#xFF0C;&#x5C31;&#x8981;&#x7279;&#x5225;&#x7559;&#x5FC3; .xcodeproj &#x7684;&#x4FEE;&#x6539;&#x554F;&#x984C;</blockquote><p>&#x8A2D;&#x5B9A;&#x5B8C;&#x4E86; code signing &#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x958B;&#x59CB;&#x4F86; build &#x8DDF; export &#x4E86;&#xFF1A;</p><pre><code class="language-swift">buildApp(
workspace: ProjectSetting.workspace,
scheme: ProjectSetting.scheme,
clean: true,
outputDirectory: &quot;./&quot;,
outputName: &quot;\(ProjectSetting.productName).ipa&quot;,
configuration: config.buildConfiguration,
silent: true,
exportMethod: config.exportMethod,
exportOptions: [
&quot;signingStyle&quot;: &quot;manual&quot;,
&quot;provisioningProfiles&quot;: [config.appIdentifier: config.provisioningProfile] ],
sdk: ProjectSetting.sdk
)</code></pre><p><a href="https://docs.fastlane.tools/actions/build_app/">buildApp</a> &#x9019;&#x500B; action&#xFF0C;&#x80FD;&#x5920;&#x5E6B;&#x4F60; build &#x4F60;&#x7684; project&#xFF0C;&#x4E26;&#x4E14;&#x8F38;&#x51FA; ipa&#xFF0C;&#x4F9B;&#x4E0A;&#x50B3;&#x6216;&#x9001;&#x5BE9;&#x3002;&#x5927;&#x90E8;&#x4EFD;&#x7684;&#x53C3;&#x6578;&#x90FD;&#x975E;&#x5E38;&#x76F4;&#x89C0;&#xFF0C;&#x50CF; configuration &#x9019;&#x88E1;&#xFF0C;&#x53EF;&#x4EE5;&#x770B;&#x51FA;&#x4F86;&#x6211;&#x5011;&#x4F7F;&#x7528; config &#x7269;&#x4EF6;&#x4F86;&#x6307;&#x5B9A;&#x6211;&#x5011;&#x8981;&#x4F7F;&#x7528;&#x7684; build configuration&#x3002;&#x53E6;&#x5916;&#xFF0C;&#x5728; exportOptions &#x9019;&#x500B;&#x53C3;&#x6578;&#xFF1A;</p><pre><code class="language-swift">
exportOptions: [
    &quot;signingStyle&quot;: &quot;manual&quot;,
    &quot;provisioningProfiles&quot;: [
    	config.appIdentifier: config.provisioningProfile
	] 
]</code></pre><p>&#x9019;&#x500B;&#x53C3;&#x6578;&#x7684;&#x578B;&#x614B;&#x662F; dictionary&#xFF0C;`signingStyle` &#x6307;&#x7684;&#x662F;&#x4F60;&#x5E0C;&#x671B;&#x4F7F;&#x7528;&#x7684; code signing &#x7684;&#x65B9;&#x6CD5;&#xFF0C;&#x9019;&#x908A;&#x5FC5;&#x9700;&#x8981;&#x8A2D;&#x5B9A;&#x6210; &quot;manual&quot;&#x3002;&#x518D;&#x4F86;&#x662F; <code>provisioningProfiles</code> &#x9019;&#x500B;&#x53C3;&#x6578;&#x6307;&#x5B9A; app id &#x8DDF; provisioning profile &#x7684; mapping&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x90A3;&#x4E00;&#x500B; app id &#x8A72;&#x4F7F;&#x7528;&#x90A3;&#x4E00;&#x500B;provisioning profile &#x53BB; sign&#xFF0C;&#x5B83;&#x4E5F;&#x662F;&#x4E00;&#x500B; dictionary&#xFF0C;key &#x662F; app id&#xFF0C;&#x800C; value &#x5C31;&#x662F; provisioning profile &#x7684;&#x6A94;&#x6848;&#x540D;&#x7A31;&#x3002;&#x5728;&#x9019;&#x908A;&#x6211;&#x5011;&#x4F7F;&#x7528; <code>config.appIdentifier: config.provisioningProfile</code>&#xFF0C;&#x4F86;&#x8B93;&#x9019;&#x500B; mapping &#x53EF;&#x4EE5;&#x7531; config &#x7269;&#x4EF6;&#x6C7A;&#x5B9A;&#x3002;</p><p>&#x4EE5;&#x4E0A;&#x6211;&#x5011;&#x5C31;&#x5B8C;&#x6210;&#x4E86;&#x5F9E; code signing &#x5230; build &#x8DDF; export &#x7684;&#x8A2D;&#x5B9A;&#x4E86;&#xFF01;</p><p>&#x73FE;&#x5728;&#x4F60;&#x53EF;&#x4EE5;&#x900F;&#x904E;</p><pre><code>bundle exec fastlane qaRelease</code></pre><p>&#x6216;&#x662F;</p><pre><code>bundle exec fastlane developerRelease</code></pre><p>&#x4F86;&#x8F38;&#x51FA;&#x91DD;&#x5C0D;&#x4E0D;&#x540C;&#x74B0;&#x5883;&#x5EFA;&#x7F6E;&#x7684; app &#x4E86;&#xFF01;</p><p>&#x4EE5;&#x4E0A;&#x662F;&#x5229;&#x7528; fastlane &#x6253;&#x5305;&#x7684;&#x90E8;&#x4EFD;&#xFF0C;&#x4F46;&#x5225;&#x5FD8;&#x4E86;&#xFF0C;&#x6211;&#x5011;&#x9084;&#x6709;&#x5B9A;&#x6642;&#x6253;&#x5305;&#x7684;&#x529F;&#x80FD;&#xFF0C;&#x9019;&#x500B;&#x4EFB;&#x52D9;&#xFF0C;&#x5C31;&#x8981;&#x4EA4;&#x7D66; Jenkins &#x4F86;&#x505A;&#x3002;</p><h2 id="the-housekeeperjenkins">The housekeeper - Jenkins</h2><p>Jenkins &#x662F;&#x4E00;&#x500B; CI/CD &#x7684;&#x7BA1;&#x7406;&#x7CFB;&#x7D71;&#xFF0C;&#x5E6B;&#x52A9;&#x4F60;&#x81EA;&#x52D5;&#x5316;&#x8207;&#x5B9A;&#x671F;&#x5316;&#x57F7;&#x884C;&#x5404;&#x7A2E;&#x4E0D;&#x540C;&#x7684;&#x4EFB;&#x52D9;&#x3002;&#x5B83;&#x6709;&#x8457;&#x61A8;&#x539A;(&#xFF1F;)&#x7684;&#x4ECB;&#x9762;&#xFF0C;&#x8B93;&#x4F60;&#x4E0D;&#x7528;&#x5BEB;&#x4E00;&#x5806; script &#x5C31;&#x80FD;&#x5920;&#x8A2D;&#x5B9A;&#x597D;&#x81EA;&#x52D5;&#x5316;&#x4EFB;&#x52D9;&#x3002;&#x5982;&#x679C;&#x4F60;&#x662F;&#x4E00;&#x500B;&#x5927;&#x5718;&#x968A;&#xFF0C;&#x5EFA;&#x8B70;&#x53E6;&#x5916;&#x5F04;&#x4E00;&#x53F0; mac mini &#x7576;&#x505A; Jenkins &#x7684;&#x4E3B;&#x6A5F;&#xFF0C;&#x6240;&#x6709;&#x767C;&#x4F48;&#x7684;&#x4EFB;&#x52D9;&#x90FD;&#x5728;&#x90A3;&#x53F0;&#x4E3B;&#x6A5F;&#x4E0A;&#x904B;&#x884C;&#xFF0C;&#x9019;&#x6A23;&#x53EF;&#x4EE5;&#x78BA;&#x4FDD;&#x6BCF;&#x6B21;&#x51FA;&#x53BB;&#x7684;&#x7248;&#x672C;&#x90FD;&#x4E0D;&#x6703;&#x56E0;&#x70BA;&#x74B0;&#x5883;&#x7684;&#x4E0D;&#x540C;&#x800C;&#x51FA;&#x73FE;&#x4E0D;&#x80FD;&#x9810;&#x671F;&#x7684;&#x554F;&#x984C;&#x3002;&#x5982;&#x679C;&#x4F60;&#x662F;&#x4E00;&#x4EBA;&#x5718;&#x968A;&#xFF0C;&#x90A3;&#x4F60;&#x5C31;&#x628A; Jenkins &#x88DD;&#x5728;&#x81EA;&#x5DF1;&#x7684;&#x96FB;&#x8166;&#xFF0C;&#x6216;&#x8005;&#x76F4;&#x63A5;&#x4F7F;&#x7528; fastlane &#x505A;&#x6253;&#x5305;&#x5C31;&#x597D;&#xFF0C;&#x6C92;&#x6709; 24 &#x5C0F;&#x6642;&#x904B;&#x884C;&#x7684;&#x4E3B;&#x6A5F;&#x7684;&#x8A71;&#xFF0C;&#x8A2D;&#x5B9A;&#x5B9A;&#x671F;&#x4EFB;&#x52D9;&#x662F;&#x6C92;&#x6709;&#x592A;&#x5927;&#x610F;&#x7FA9;&#x7684;(&#x70BA;&#x4E86;&#x5728; build code &#x6642;&#x80FD;&#x9806;&#x9806;&#x5730;&#x770B; Netflix&#xFF0C;&#x6211;&#x4E5F;&#x597D;&#x60F3;&#x8981;&#x4E00;&#x53F0; mac mini)&#x3002;</p><p>Jenkins &#x5728; mac &#x4E0A;&#x5B89;&#x88DD;&#x975E;&#x5E38;&#x5BB9;&#x6613;&#xFF0C;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x900F;&#x904E; dmg &#x6A94;&#x5B89;&#x88DD;&#xFF0C;<a href="https://jenkins.io/download/">Jenkins installation and setup</a>&#x3002;&#x53E6;&#x5916;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x9700;&#x8981; <a href="https://plugins.jenkins.io/git">git</a> &#x7684; plugin&#xFF0C;&#x8B93; Jenkins &#x80FD;&#x5920;&#x652F;&#x63F4; git &#x7684;&#x64CD;&#x4F5C;&#x3002;dmg &#x5B89;&#x88DD;&#x6703;&#x8A2D;&#x5B9A;&#x4E00;&#x500B; mac &#x7684; user: jenkins&#xFF0C;&#x5B83;&#x4E0D;&#x80FD;&#x767B;&#x5165;&#x4E5F;&#x4E0D;&#x80FD;&#x64CD;&#x4F5C; commandline&#xFF0C;&#x4E5F;&#x4E0D;&#x4F54;&#x592A;&#x591A;&#x7A7A;&#x9593;&#xFF0C;&#x7D14;&#x7CB9;&#x5C31;&#x662F;&#x8B93; Jenkins server &#x6709;&#x7368;&#x7ACB;&#x7684;&#x6B0A;&#x9650;&#x63A7;&#x7BA1;&#x3002;</p><p>&#x5B89;&#x88DD;&#x5B8C;&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5148;&#x4F86;&#x770B;&#x770B;&#xFF0C;Jenkins &#x5728;&#x6211;&#x5011;&#x7684; CD&#x4E2D;&#xFF0C;&#x626E;&#x6F14;&#x600E;&#x6A23;&#x7684;&#x89D2;&#x8272;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/img2.jpeg" class="kg-image" alt="img2.jpeg" loading="lazy"></figure><p>&#x5F9E;&#x5716;&#x4E0A;&#x53EF;&#x4EE5;&#x770B;&#x5F97;&#x51FA;&#x4F86;&#xFF0C;Jenkins &#x626E;&#x6F14;&#x8457;&#x7BA1;&#x5BB6;&#x7684;&#x89D2;&#x8272;&#xFF0C;&#x6642;&#x9593;&#x4E00;&#x5230;&#xFF0C;&#x5B83;&#x5C31;&#x6703;&#x8CA0;&#x8CAC;&#x53BB; repository &#x6293;&#x6700;&#x65B0;&#x7684;&#x539F;&#x59CB;&#x78BC;&#xFF0C;&#x6293;&#x4E0B;&#x4F86;&#x4E4B;&#x5F8C;&#xFF0C;&#x958B;&#x59CB;&#x57F7;&#x884C;&#x6307;&#x5B9A;&#x7684;&#x4EFB;&#x52D9;&#xFF0C;&#x4EFB;&#x52D9;&#x5167;&#x5BB9;&#x5305;&#x62EC;&#xFF1A;</p><ol><li>&#x8A2D;&#x5B9A; environment variables</li><li>&#x900F;&#x904E; bundler &#x5B89;&#x88DD; dependency</li><li>&#x57F7;&#x884C; fastlane &#x7684;&#x4EFB;&#x52D9;</li></ol><p>&#x4E86;&#x89E3;&#x4E86; Jenkins &#x7684;&#x5DE5;&#x4F5C;&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x958B;&#x59CB;&#x4F86;&#x65B0;&#x589E;&#x6211;&#x5011;&#x7684;&#x5B9A;&#x671F;&#x4EFB;&#x52D9;&#x3002;&#x5148;&#x5F9E; nightly build &#x958B;&#x59CB;&#xFF0C;&#x6211;&#x5011;&#x5148;&#x65B0;&#x589E;&#x4E00;&#x500B; Jenkins freestyle project&#xFF0C;&#x4E26;&#x9EDE;&#x64CA; <code>Configure</code> &#x9032;&#x5230;&#x8A2D;&#x5B9A;&#x9801;&#x9762;&#x3002;&#x8A2D;&#x5B9A;&#x9801;&#x9762;&#x88E1;&#x6709;&#x5E7E;&#x500B;&#x91CD;&#x9EDE;&#x6211;&#x5011;&#x9700;&#x8981;&#x6CE8;&#x610F;&#xFF1A;</p><p>&#x7B2C;&#x4E00;&#x500B;&#x662F; <strong>Source Code Management</strong>(SCM)&#xFF0C;&#x5728;&#x9019;&#x908A;&#x6211;&#x5011;&#x6703;&#x8A2D;&#x5B9A;&#x6211;&#x5011; project &#x7684;repository URL&#xFF0C;&#x9084;&#x6709;&#x6307;&#x5B9A;&#x8981; fetch&#x7684;branch&#xFF0C;&#x5982;&#x4E0B;&#x5716;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-17-at-23-15-31.png" class="kg-image" alt="Screen Shot 2018-03-17 at 23.15.31.png" loading="lazy"></figure><p><strong>Repository URL </strong>&#x5C31;&#x662F;&#x8A2D;&#x5B9A;&#x6211;&#x5011;&#x7684; github &#x6216;&#x5176;&#x5B83;&#x7CFB;&#x7D71;&#x7684; repository url&#xFF0C;<strong>Credentials </strong>&#x5247;&#x662F;&#x53EF;&#x4EE5;&#x8A2D;&#x5B9A;&#x4F60;&#x5728; repository &#x7684;&#x5E33;&#x865F;&#x8DDF;&#x5BC6;&#x78BC;&#xFF0C;&#x9019;&#x6A23; Jenkins &#x624D;&#x80FD;&#x901A;&#x904E;&#x6388;&#x6B0A;&#x4E0B;&#x8F09;&#x539F;&#x59CB;&#x78BC;(&#x5982;&#x679C;&#x662F; private repository)&#x3002;<strong>Branches to build </strong>&#x53EF;&#x4EE5;&#x8A2D;&#x5B9A;&#x4F60;&#x7684;&#x76EE;&#x6A19; branch&#xFF0C;&#x4EE5;&#x5B9A;&#x671F;&#x4EFB;&#x52D9;&#x4F86;&#x8AAA;&#xFF0C;&#x901A;&#x5E38;&#x90FD;&#x6703;&#x8A2D;&#x5B9A;&#x6210;&#x4F60;&#x7684; default branch&#x3002;</p><p>&#x518D;&#x5F80;&#x4E0B;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x770B;&#x5230; <strong>Builder Trigger </strong>&#x5340;&#x584A;&#xFF0C;&#x9019;&#x500B;&#x5340;&#x584A;&#x662F;&#x8981;&#x8A2D;&#x5B9A;&#x9019;&#x500B;&#x4EFB;&#x52D9;&#x7684;&#x89F8;&#x767C;&#x9EDE;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8981;&#x8A2D;&#x5B9A;&#x81EA;&#x52D5;&#x5316;&#x555F;&#x52D5;&#x4EFB;&#x52D9;&#x7684;&#x689D;&#x4EF6;&#x3002;&#x5982;&#x679C;&#x9019;&#x908A;&#x90FD;&#x6C92;&#x6709;&#x8A2D;&#x5B9A;&#x4EFB;&#x4F55; trigger&#xFF0C;&#x90A3;&#x4EFB;&#x52D9;&#x5C31;&#x662F;&#x624B;&#x52D5;&#x89F8;&#x767C;&#x3002;&#x6211;&#x5011;&#x7684; trigger &#x8A2D;&#x5B9A;&#x5982;&#x4E0B;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-17-at-23-28-02.png" class="kg-image" alt="Screen Shot 2018-03-17 at 23.28.02.png" loading="lazy"></figure><p>&#x6211;&#x5011;&#x555F;&#x52D5;&#x4E86;&#x4E00;&#x500B; <strong>Poll SCM</strong>&#xFF0C;&#x5B83;&#x7684;&#x610F;&#x601D;&#x662F;&#xFF0C;&#x8B93; Jenkins &#x5B9A;&#x671F;&#x5411;&#x6211;&#x5011;&#x7684; source code repository &#x67E5;&#x770B;&#x662F;&#x5426;&#x6709;&#x66F4;&#x65B0;&#x8CC7;&#x6599;&#xFF0C;&#x5982;&#x679C;&#x6709;&#x7684;&#x8A71;&#xFF0C;&#x5C31;&#x555F;&#x52D5;&#x6211;&#x5011;&#x73FE;&#x5728;&#x9019;&#x500B; task&#x3002;&#x8A2D;&#x5B9A;&#x662F;&#x4E00;&#x4E32;&#x7A7A;&#x767D;&#x9694;&#x958B;&#x7684;&#x706B;&#x661F;&#x6587;&#xFF1A;</p><pre><code>H 0 * * 0-4</code></pre><p>&#x9019;&#x662F;&#x751A;&#x9EBC;&#x610F;&#x601D;&#xFF1F;&#x8B93;&#x6211;&#x5011;&#x5148;&#x770B;&#x4E00;&#x4E0B;&#xFF0C;&#x95DC;&#x65BC; <strong>Poll SCM </strong>&#x7684;&#x8AAA;&#x660E;&#xFF1A;</p><pre><code class="language-none">This field follows the syntax of cron (with minor differences). Specifically, each line consists of 5 fields separated by TAB or whitespace:
MINUTE HOUR DOM MONTH DOW
MINUTE  Minutes within the hour (0&#x2013;59)
HOUR    The hour of the day (0&#x2013;23)
DOM The day of the month (1&#x2013;31)
MONTH   The month (1&#x2013;12)
DOW The day of the week (0&#x2013;7) where 0 and 7 are Sunday.</code></pre><p>&#x5F9E;&#x8AAA;&#x660E;&#x53EF;&#x4EE5;&#x770B;&#x5F97;&#x51FA;&#x4F86;&#xFF0C;&#x9019;&#x4E00;&#x4E32;&#x5B57;&#x4E32;&#x7E3D;&#x5171;&#x6709;&#x4E94;&#x500B;&#x90E8;&#x4EFD;&#xFF0C;&#x7531;&#x5DE6;&#x5230;&#x53F3;&#x4F9D;&#x5E8F;&#x662F;&#xFF1A;</p><ul><li>&#x5206;</li><li>&#x6642;</li><li>&#x65E5;</li><li>&#x6708;</li><li>&#x661F;&#x671F;&#x5E7E;</li></ul><p>&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x6307;&#x5B9A;&#x6578;&#x5B57;&#xFF0C;&#x6216;&#x8005;<code>0&#x2013;59</code>&#x9019;&#x6A23;&#x7684;&#x5B57;&#x4E32;&#x4EE3;&#x8868;&#x6709;&#x6548;&#x5340;&#x9593;&#xFF0C;&#x4F7F;&#x7528;*&#x7684;&#x8A71;&#x5C31;&#x8868;&#x793A;&#x6240;&#x6709;&#x53EF;&#x80FD;&#x7684;&#x6578;&#x503C;&#x90FD;&#x6703;&#x89F8;&#x767C;&#xFF0C;&#x4EE3;&#x8868;&#x4EFB;&#x610F;&#x4E00;&#x500B;&#x6709;&#x6548;&#x7684;&#x6578;&#x5B57;&#xFF0C;&#x901A;&#x5E38;&#x6703;&#x88AB;&#x7528;&#x5728;&#x5206;&#x7684;&#x8A2D;&#x5B9A;&#x4E0A;&#xFF0C;&#x76EE;&#x5730;&#x662F;&#x8981;&#x628A;&#x7CFB;&#x7D71;&#x4E0A;&#x6BCF;&#x4E00;&#x500B; task &#x90FD;&#x76E1;&#x91CF;&#x5206;&#x6563;&#x5728;&#x4E00;&#x5C0F;&#x6642;&#x5167;&#x904B;&#x884C;&#x3002;&#x6240;&#x4EE5;&#x56DE;&#x5230;&#x6211;&#x5011;&#x525B;&#x525B;&#x7684; schdule setting&#xFF1A;</p><pre><code>H 0 * * 0-4</code></pre><p>&#x8868;&#x793A;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x4EFB;&#x52D9;&#x53EF;&#x4EE5;&#x5728;&#x9031;&#x65E5;&#x5230;&#x9031;&#x56DB;&#x3001;&#x4E00;&#x5E74;&#x4E2D;&#x6240;&#x6709;&#x7684;&#x6708;&#x4EFD;&#x3001;&#x6BCF;&#x5929;&#x7684; 0 &#x9EDE;&#x4E0D;&#x6307;&#x5B9A;&#x5206;&#x57F7;&#x884C;&#xFF0C;&#x9019;&#x5C31;&#x662F; nightly build &#x7684; schedule&#x3002;</p><p>&#x6700;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5F80;&#x4E0B;&#x53EF;&#x4EE5;&#x770B;&#x5230; <strong>Build </strong>&#x7684;&#x5340;&#x584A;&#xFF0C;&#x5728;&#x9019;&#x88E1;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x8A2D;&#x5B9A;&#x7576;&#x6642;&#x9593;&#x4E00;&#x5230;&#xFF0C;&#x8981;&#x8B93;Jenkis&#x57F7;&#x884C;&#x7684;&#x7A0B;&#x5F0F;&#x3002;&#x5167;&#x5BB9;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-shell">export LC_ALL=en_US.UTF-8;
export LANG=en_US.UTF-8;

export CODESIGNING_PATH=&quot;/path/to/cert&quot;;
export CERTIFICATE_PASSWORD=&quot;xxx&quot;;
export KEYCHAIN_NAME=&quot;XXXXXXXX&quot;;
export KEYCHAIN_PASSWORD=&quot;xxxxxxxxxxxxxx&quot;

bundle install --path vendor/bundler
bundle exec fastlane developerRelease</code></pre><p>&#x524D;&#x9762;&#x5E7E;&#x884C;&#x90FD;&#x662F;&#x5728;&#x8A2D;&#x5B9A; environment &#x8B8A;&#x6578;&#xFF0C;&#x50CF;&#x662F; LC_ALL &#x8DDF; LANG &#x4E3B;&#x8981;&#x662F;&#x8981;&#x78BA;&#x4FDD;fastlane&#x80FD;&#x904B;&#x884C;&#x5728;&#x6B63;&#x78BA;&#x7684;locale&#x5E95;&#x4E0B;&#xFF0C;&#x800C; KEYCHAIN_NAME&#x3001;KEYCHAIN_PASSWORD &#x8207; CERTIFICATE_PASSWORD &#x5C31;&#x662F;&#x6211;&#x5011;&#x5728; fasten &#x8A2D;&#x5B9A;&#x6559;&#x5B78;&#x6642;&#x4F7F;&#x7528;&#x7684;&#x74B0;&#x5883;&#x8B8A;&#x6578;&#x3002;CERT_PATH &#x662F;&#x4F60;&#x653E;&#x7F6E;&#x5728; Jenkins &#x4E3B;&#x6A5F;&#x4E0A;&#x7684; code signing &#x76EE;&#x9304;&#x7684;&#x4F4D;&#x7F6E;&#x3002;&#x9019;&#x88E1;&#x6211;&#x5011;&#x6703;&#x5EFA;&#x8B70;&#x4F7F;&#x7528;&#x7D55;&#x5C0D;&#x8DEF;&#x5F91;&#xFF0C;&#x7D93;&#x9A57;&#x4E0A;&#xFF0C;&#x6709;&#x6642;&#x5019;&#x8A2D;&#x5B9A;&#x76F8;&#x5C0D;&#x8DEF;&#x5F91;&#x5728;&#x67D0;&#x4E9B; action &#x4E0A;&#x9762;&#x662F;&#x7121;&#x6CD5;&#x904B;&#x4F5C;&#x7684;&#x3002; &#x6700;&#x5F8C;&#x5169;&#x884C;&#x5247;&#x662F;&#x5B89;&#x88DD; dependency &#x8DDF;&#x771F;&#x6B63;&#x57F7;&#x884C; fastlane &#x4F86;build&#x4F60;&#x7684;project&#x3002;&#x9019;&#x908A;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x770B;&#x5F97;&#x51FA;&#x4F86;&#xFF0C;&#x6211;&#x5011;&#x7684; nightly build task &#x6703;&#x57F7;&#x884C; developerRelease &#x9019;&#x500B; lane&#x3002;</p><p>&#x5230;&#x9019;&#x908A;&#xFF0C;&#x6211;&#x5011;&#x7D42;&#x65BC;&#x628A;&#x6211;&#x5011;&#x7684; nightly build &#x5EFA;&#x7ACB;&#x8D77;&#x4F86;&#x4E86;&#xFF01;&#x4F60;&#x53EF;&#x4EE5;&#x5728; Jenkins &#x7684; project &#x9801;&#x9762;&#x9EDE;&#x64CA; build now &#x4F86;&#x770B;&#x770B;&#x4F60;&#x7684;&#x4EFB;&#x52D9;&#x662F;&#x5426;&#x6709;&#x904B;&#x884C;&#x6210;&#x529F;&#x3002;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-18-at-00-06-57.png" class="kg-image" alt="Screen Shot 2018-03-18 at 00.06.57.png" loading="lazy"></figure><p>&#x5728;&#x5E95;&#x4E0B;&#x7684; Build history&#xFF0C;&#x4F60;&#x53EF;&#x4EE5;&#x770B;&#x5230;&#x6BCF;&#x4E00;&#x6B21; build &#x7684;&#x6642;&#x9593;&#x8207;&#x6210;&#x529F;&#x6216;&#x5931;&#x6557;&#x7684;&#x8A18;&#x9304;&#xFF0C;&#x9EDE;&#x64CA; build number &#x53EF;&#x4EE5;&#x770B;&#x5230;&#x66F4;&#x8A73;&#x7D30;&#x7684;&#x72C0;&#x614B;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x770B;&#x5230; console &#x7684;&#x8F38;&#x51FA;&#x3002;&#x9019;&#x4E9B;&#x529F;&#x80FD;&#x90FD;&#x53EF;&#x4EE5;&#x8B93;&#x4F60;&#x5F88;&#x65B9;&#x4FBF;&#x5730;&#x4E86;&#x89E3; build &#x7684;&#x72C0;&#x6CC1;&#x9084;&#x6709;&#x5982;&#x679C;&#x767C;&#x751F;&#x554F;&#x984C;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x80FD;&#x5920;&#x66F4;&#x5FEB;&#x5730;&#x627E;&#x5230;&#x554F;&#x984C;&#x4E26;&#x89E3;&#x6C7A;&#x3002;</p><h2 id="%E7%82%BA%E7%94%9A%E9%BA%BC%E4%BB%BB%E5%8B%99%E4%B8%80%E7%9B%B4%E5%A4%B1%E6%95%97%EF%BC%9F">&#x70BA;&#x751A;&#x9EBC;&#x4EFB;&#x52D9;&#x4E00;&#x76F4;&#x5931;&#x6557;&#xFF1F;</h2><p>&#x53EA;&#x8981;&#x662F;&#x8DDF;&#x7CFB;&#x7D71;&#x76F8;&#x95DC;&#x7684;&#x554F;&#x984C;&#xFF0C;&#x901A;&#x5E38;&#x72C0;&#x6CC1;&#x90FD;&#x975E;&#x5E38;&#x591A;&#x4E14;&#x8907;&#x96DC;&#xFF0C;&#x96D6;&#x7136;&#x5927;&#x591A;&#x7684;&#x932F;&#x8AA4;&#x90FD;&#x53EF;&#x4EE5;&#x5728; google &#x4E0A;&#x627E;&#x5230;&#x89E3;&#x7B54;&#xFF0C;&#x4F46;&#x662F;&#x6709;&#x6642;&#x5019;&#x4F60;&#x5F97;&#x5230;&#x7684;&#x932F;&#x8AA4;&#x8A0A;&#x606F;&#x4E26;&#x4E0D;&#x662F;&#x6709;&#x7528;&#x7684;&#x6216;&#x662F;&#x76F8;&#x95DC;&#x7684;&#x3002;&#x4EE5;&#x4E0B;&#x6709;&#x5E7E;&#x9EDE;&#x5C0F;&#x6280;&#x5DE7;&#xFF0C;&#x8B93;&#x4F60;&#x5728;&#x9047;&#x5230;&#x554F;&#x984C;&#x6642;&#x53EF;&#x4EE5;&#x66F4;&#x5FEB;&#x7372;&#x5F97;&#x89E3;&#x7B54;&#x3002;</p><h3 id="unlock-your-keychain">Unlock your KeyChain</h3><p>&#x4E0D;&#x7BA1;&#x4F60;&#x60F3;&#x628A; certificate &#x5B58;&#x5728;&#x90A3;&#x4E00;&#x500B;keychain&#x88E1;&#xFF0C;&#x90FD;&#x8981;&#x8A18;&#x5F97;&#x89E3;&#x9396;&#x5B83;&#x5662;&#x3002;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/screen-shot-2018-03-18-at-00-12-21.png" class="kg-image" alt="Screen Shot 2018-03-18 at 00.12.21.png" loading="lazy"></figure><p>&#x60F3;&#x8981;&#x4E86;&#x89E3;&#x66F4;&#x9032;&#x968E;&#x7684; keychain &#x7522;&#x751F;&#x6280;&#x5DE7;&#xFF0C;&#x907F;&#x514D;&#x52D5;&#x5230;&#x9810;&#x8A2D; login keychain&#xFF0C;&#x9084;&#x6709;&#x6E1B;&#x5C11;certificate &#x88AB;&#x5F9E; keychain copy &#x51FA;&#x4F86;&#x7684;&#x98A8;&#x96AA;&#xFF0C;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x5C0F;&#x5F1F;&#x7684; github project&#xFF0C;&#x88E1;&#x9762;&#x7684; fastfile &#x662F;&#x81EA;&#x52D5;&#x7522;&#x751F; keychain &#x4E26;&#x4E14;&#x5728;&#x4F7F;&#x7528;&#x904E;&#x5F8C;&#x522A;&#x9664; keychain &#x7684;&#x7BC4;&#x4F8B;&#x3002;</p><h3 id="%E5%85%88%E7%A2%BA%E4%BF%9D%E8%83%BD%E5%9C%A8-jenkins-%E4%B8%BB%E6%A9%9F%E8%83%BD%E5%A4%A0-build-export">&#x5148;&#x78BA;&#x4FDD;&#x80FD;&#x5728; Jenkins &#x4E3B;&#x6A5F;&#x80FD;&#x5920; build &amp; export</h3><p>fastlane &#x5176;&#x5BE6;&#x662F;&#x628A; Xcode commandline tool &#x6253;&#x5305;&#x6210;&#x4EBA;&#x985E;&#x6BD4;&#x8F03;&#x597D;&#x7406;&#x89E3;&#x7684;&#x8A9E;&#x8A00;&#xFF0C;&#x6240;&#x4EE5;&#x901A;&#x5E38;&#x5982;&#x679C;&#x4E0D;&#x80FD; build&#xFF0C;&#x6709;&#x5F88;&#x5927;&#x7684;&#x6A5F;&#x6703;&#x662F;&#x56E0;&#x70BA; Xcode &#x672C;&#x8EAB;&#x5C31; build &#x4E0D;&#x904E;&#xFF0C;&#x6240;&#x4EE5;&#x5982;&#x679C; Jenkins &#x7684;&#x4EFB;&#x52D9;&#x4E0D;&#x904E;&#xFF0C;&#x8ACB;&#x5148;&#x4E0D;&#x8981;&#x6025;&#x8457;&#x5728; Jenkins &#x7684;&#x4ECB;&#x9762;&#x4E0A; debug&#xFF0C;&#x5148;&#x5728; Jenkins &#x4E3B;&#x6A5F;&#x4E0A;&#xFF0C;&#x900F;&#x904E; commandline build &#x4F60;&#x7684; project&#xFF0C;&#x9019;&#x6A23;&#x53EF;&#x4EE5;&#x5F97;&#x5230;&#x66F4;&#x591A;&#x6709;&#x7528;&#x7684;&#x8CC7;&#x8A0A;&#x3002;</p><pre><code>xcodebuild clean archive -workspace &lt;your workspace file&gt; -scheme &lt;your scheme&gt;</code></pre><p>&#x4F60;&#x53EF;&#x4EE5;&#x52A0;&#x4E0A; <code>&#x2014;verbose</code> &#x53C3;&#x6578;&#xFF0C;&#x8B93;&#x8F38;&#x51FA;&#x7684;&#x8A0A;&#x606F;&#x66F4;&#x5B8C;&#x6574;&#x3002;</p><p>fastlane &#x900F;&#x904E; parse &#x4F60;&#x7684; build setting &#x4F86;&#x6C7A;&#x5B9A;&#x8981;&#x600E;&#x6A23;&#x57F7;&#x6210;&#x4EFB;&#x52D9;&#xFF0C;&#x6240;&#x4EE5;&#x5982;&#x679C;&#x4F60;&#x60F3;&#x78BA;&#x5B9A; build &#x8A2D;&#x5B9A;&#x662F;&#x4E0D;&#x662F;&#x6B63;&#x78BA;&#xFF0C;&#x53EF;&#x4EE5;&#x4F7F;&#x7528;&#x4EE5;&#x4E0B;&#x6307;&#x4EE4;&#x4F86;&#x6AA2;&#x67E5;&#xFF1A;</p><pre><code>xcodebuild -showBuildSettings -workspace &lt;your workspace file&gt; -scheme &lt;your scheme&gt; -configuration &lt;the configuration you want to check&gt;</code></pre><p>&#x7576;&#x4F60; achieve &#x5B8C;&#x4E4B;&#x5F8C;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x6E2C;&#x8A66;&#x4E00;&#x4E0B;&#x662F;&#x5426;&#x80FD;&#x5920; export &#x6210;&#x529F;&#xFF1A;</p><pre><code class="language-shell">xcodebuild -exportArchive \
           -exportFormat ipa \
           -archivePath &lt;ARCH PATH&gt;&quot; \
           -exportPath &lt;IPA PATH&gt; \
           -exportProvisioningProfile &quot;&lt;Provisioning Profile&gt;&quot;</code></pre><h2 id="cd-%E7%B3%BB%E7%B5%B1%E7%9A%84%E8%A8%AD%E8%A8%88%E5%8E%9F%E5%89%87">CD &#x7CFB;&#x7D71;&#x7684;&#x8A2D;&#x8A08;&#x539F;&#x5247;</h2><p>&#x4EE5; iOS/macOS &#x7684; CD &#x7CFB;&#x7D71;&#x4F86;&#x8AAA;&#xFF0C;&#x6E1B;&#x5C11; bug &#x4E26;&#x4E14;&#x63D0;&#x65E9;&#x4E0B;&#x73ED;(&#x6216;&#x52D9;&#x5BE6;&#x4E00;&#x9EDE;&#xFF0C;&#x6E96;&#x6642;&#x4E0B;&#x73ED;)&#xFF0C;&#x901A;&#x5E38;&#x6709;&#x5E7E;&#x500B;&#x8981;&#x9EDE;&#xFF1A;</p><ul><li>&#x6E1B;&#x5C11;&#x7CFB;&#x7D71;&#x76F8;&#x4F9D;&#x6027;&#xFF1A;&#x76E1;&#x91CF;&#x4E0D;&#x8981;&#x4F9D;&#x8CF4;&#x67D0;&#x500B;&#x7CFB;&#x7D71;&#x7684;&#x7A0B;&#x5F0F;&#xFF0C;&#x50CF;&#x662F;&#x9700;&#x8981; ruby &#x7684;&#x67D0;&#x500B;&#x7248;&#x672C;&#xFF0C;&#x6216;&#x8005;&#x547C;&#x53EB;&#x67D0;&#x500B;&#x5728; <code>usr/local/bin</code> &#x7684;&#x7A0B;&#x5F0F;&#x3002;</li><li>&#x4EFB;&#x52D9;&#x8DDF;&#x4EFB;&#x52D9;&#x4E4B;&#x9593;&#x4E0D;&#x80FD;&#x6709;&#x76F8;&#x4F9D;&#x6027;&#xFF1A;&#x6BCF;&#x6B21;&#x7684;&#x4EFB;&#x52D9;&#x90FD;&#x662F;&#x5168;&#x65B0;&#x7684;&#x958B;&#x59CB;&#xFF0C;&#x4E0D;&#x80FD;&#x6709;&#x4E0B;&#x6B21;&#x4EFB;&#x52D9;&#x7528;&#x5230;&#x4E0A;&#x6B21;&#x4EFB;&#x52D9;&#x7522;&#x51FA;&#x7684;&#x8CC7;&#x6599;&#x7684;&#x60C5;&#x6CC1;&#x767C;&#x751F;&#xFF0C;&#x6700;&#x597D;&#x7684;&#x505A;&#x6CD5;&#x5C31;&#x662F;&#x57F7;&#x884C;&#x5B8C;&#x4EFB;&#x52D9;&#x4E4B;&#x5F8C;&#xFF0C;&#x5C31;&#x628A;&#x4E2D;&#x9593;&#x7522;&#x7269;&#x522A;&#x9664;&#x3002;</li><li>CD &#x7CFB;&#x7D71;&#x8981;&#x80FD;&#x5920;&#x88AB;&#x5F88;&#x5FEB;&#x5730;&#x8907;&#x88FD;&#xFF1A;&#x5C31;&#x7B97;&#x8F49;&#x63DB;&#x5230;&#x4E0D;&#x540C;&#x96FB;&#x8166;&#x4E0A;&#xFF0C;&#x61C9;&#x8A72;&#x4E5F;&#x8981;&#x80FD;&#x5920;&#x5728;&#x88DD;&#x5B8C; Jenkins &#x5F8C;&#x5C31;&#x76F4;&#x63A5;&#x88AB;&#x57F7;&#x884C;&#xFF0C;&#x50CF;&#x662F;&#x628A;&#x8DDF; project &#x7121;&#x95DC;&#x7684;&#x8A2D;&#x5B9A;&#xFF0C;&#x5982; path &#x8DDF; password&#xFF0C;&#x90FD;&#x79FB;&#x5230; environment variables&#xFF0C;&#x9019;&#x6A23;&#x4EFB;&#x52D9;&#x51FA;&#x932F;&#x6642;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x80FD;&#x5920;&#x628A;&#x5C08;&#x6CE8;&#x529B;&#x653E;&#x5728;&#x6211;&#x5011;&#x7684;&#x7A0B;&#x5F0F;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x67D0;&#x500B;&#x4E3B;&#x6A5F;&#x7684;&#x8A2D;&#x5B9A;&#x4E0A;&#x3002;</li></ul><p>&#x6700;&#x5F8C;&#xFF0C;&#x4F60;&#x4E5F;&#x53EF;&#x4EE5;&#x53C3;&#x8003; fastlane &#x7684; troubleshooting&#xFF0C;&#x9019;&#x88E1;&#x9762;&#x6709;&#x8A31;&#x591A;&#x6709;&#x7528;&#x7684;&#x8CC7;&#x8A0A;&#xFF1A;<a href="https://docs.fastlane.tools/codesigning/troubleshooting/">Troubleshooting - fastlane docs</a>&#x3002; &#x53EF;&#x4EE5;&#x770B;&#x5230;&#xFF0C;&#x5927;&#x591A;&#x6578;&#x7684;&#x554F;&#x984C;&#xFF0C;&#x90FD;&#x662F;&#x842C;&#x60E1;&#x7684; code signing &#x5F15;&#x8D77;&#x7684;&#xFF0C;iOS &#x5DE5;&#x7A0B;&#x5E2B;&#x901A;&#x5E38;&#x6703;&#x5728;&#x4E00;&#x5929;&#x7684;&#x4E00;&#x65E9;&#x958B;&#x59CB;&#x8655;&#x7406;&#x4E0D;&#x80FD; build &#x7684;&#x554F;&#x984C;&#xFF0C;&#x4E26;&#x4E14;&#x5728;&#x4E0B;&#x73ED;&#x524D;&#x4E94;&#x5206;&#x9418;&#x767C;&#x73FE;&#x539F;&#x4F86;&#x662F;&#x7528;&#x5230;&#x4E86;&#x820A;&#x7684; certificate&#xFF0C;&#x7136;&#x5F8C;&#x6D41;&#x8457;&#x6DDA;&#x5728;&#x516C;&#x53F8;&#x52A0;&#x73ED;&#x3002;</p><h2 id="summary">Summary</h2><p>&#x96D6;&#x7136; iOS/macOS &#x5DE5;&#x7A0B;&#x5E2B;&#xFF0C;&#x6709;&#x5F88;&#x5927;&#x4E00;&#x90E8;&#x4EFD;&#x90FD;&#x662F;&#x4E00;&#x4EBA;&#x5718;&#x968A;&#xFF0C;&#x6240;&#x4EE5;&#x6240;&#x8B02;&#x7684; delivery &#x4E5F;&#x5C31;&#x662F; archive &#x5F8C;&#xFF0C;&#x76F4;&#x63A5;&#x9EDE; Crashlytics &#x7684; distribute &#x5C31;&#x9001;&#x51FA;&#x4E86;&#x3002;&#x4F46;&#x662F;&#x5C31;&#x7B97;&#x662F;&#x4E00;&#x4EBA;&#x5718;&#x968A;&#xFF0C;&#x900F;&#x904E;&#x4E00;&#x500B;&#x81EA;&#x52D5;&#x5316;&#x7CFB;&#x7D71;&#xFF0C;&#x5C31;&#x80FD;&#x5920;&#x628A; build &#x4EFB;&#x52D9;&#x5206;&#x6524;&#x5230;&#x5225;&#x7684;&#x4E3B;&#x6A5F;&#xFF0C;&#x6E1B;&#x5C11; build &#x6642;&#x5207;&#x63DB;&#x74B0;&#x5883;&#x767C;&#x751F;&#x7684;&#x932F;&#x8AA4;&#xFF0C;&#x9084;&#x662F;&#x975E;&#x5E38;&#x503C;&#x5F97;&#x6295;&#x8CC7;&#x7684;&#x3002;&#x66F4;&#x4E0D;&#x7528;&#x8AAA;&#x5728;&#x591A;&#x4EBA;&#x5718;&#x968A;&#xFF0C;&#x8DDF; workflow &#x7D50;&#x5408;&#x7684; delivery system&#xFF0C;&#x80FD;&#x5920;&#x5E36;&#x4F86;&#x7684;&#x597D;&#x8655;&#x5C31;&#x66F4;&#x591A;&#x4E86;&#x3002;&#x5982;&#x679C;&#x4F60;&#x4E0D;&#x559C;&#x6B61;&#x5BEB; script &#x6216;&#x8DDF;&#x7CFB;&#x7D71;&#x6253;&#x4EA4;&#x9053;&#xFF0C;&#x76EE;&#x524D; Apple &#x751F;&#x614B;&#x7CFB;&#x7684; CD &#x7CFB;&#x7D71;&#x5176;&#x5BE6;&#x4E5F;&#x975E;&#x5E38;&#x84EC;&#x52C3;&#xFF0C;&#x5305;&#x62EC;&#x500B;&#x4EBA;&#x89BA;&#x5F97;&#x76F8;&#x7576;&#x597D;&#x7528;&#x7684; bitrise&#x3001;&#x5F88;&#x591A;&#x4EBA;&#x90FD;&#x5728;&#x4F7F;&#x7528;&#x7684; CircleCI&#x3001;&#x9084;&#x6709;&#x88AB; Apple &#x8CB7;&#x8D70;&#x7684; Buddybuild&#xFF08;&#x671F;&#x5F85;apple&#x539F;&#x751F;&#x7684;CD&#x74B0;&#x5883;&#xFF09;&#xFF0C;&#x90FD;&#x662F;&#x975E;&#x5E38;&#x53EF;&#x4EE5;&#x8003;&#x616E;&#x7684;&#x9078;&#x9805;&#x3002;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E3B;&#x8981;&#x8457;&#x773C;&#x5728;&#x4E86;&#x89E3;&#x5982;&#x4F55;&#x900F;&#x904E; fastlane &#x8DDF; Jenkins &#x5EFA;&#x7F6E;&#x7C21;&#x55AE;&#x7684; CD &#x7CFB;&#x7D71;&#xFF0C;&#x5982;&#x679C;&#x4F60;&#x90FD;&#x4E86;&#x89E3;&#x7684;&#x8A71;&#xFF0C;&#x4E00;&#x5B9A;&#x53EF;&#x4EE5;&#x8A2D;&#x8A08;&#x51FA;&#x4E00;&#x5957;&#x7B26;&#x5408;&#x4F60;&#x76EE;&#x524D;&#x5718;&#x968A;&#x5DE5;&#x4F5C;&#x6D41;&#x7A0B;&#x7684; CD &#x7CFB;&#x7D71;&#x7684;&#xFF01;</p><p>&#x597D;&#x4E86;&#xFF0C;&#x524D;&#x60C5;&#x63D0;&#x8981;&#x771F;&#x7684;&#x592A;&#x4E45;&#x4E86;&#xFF0C;&#x672C;&#x6587;&#x958B;&#x59CB;&#x3002;</p><hr><h2 id="black-mirror">Black Mirror</h2><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2018/03/mv5bmjg1nteymdi3mv5bml5banbnxkftztgwntg2mzgzmdi-_v1_sx1500_cr001500999_al_1.jpg" class="kg-image" alt="MV5BMjg1NTEyMDI3MV5BMl5BanBnXkFtZTgwNTg2MzgzMDI@._V1_SX1500_CR0,0,1500,999_AL_.jpg" loading="lazy"></figure><p>&#x8EAB;&#x70BA;&#x4E00;&#x500B;&#x53CD;&#x70CF;&#x6258;&#x90A6;&#x8166;&#x7C89;&#xFF0C;&#x4E00;&#x5B9A;&#x8981;&#x63A8;&#x85A6;&#x4E00;&#x4E0B;&#x9019;&#x90E8;&#x53CD;&#x70CF;&#x6258;&#x90A6;&#x7D93;&#x5178;&#x5F71;&#x96C6;&#xFF1A;Black Mirror&#x3002;&#x9019;&#x662F;&#x4E00;&#x90E8;&#x82F1;&#x570B;&#x7684;&#x5F71;&#x96C6;&#xFF0C;&#x76EE;&#x524D;&#x5DF2;&#x7D93;&#x51FA;&#x5230;&#x7B2C;&#x56DB;&#x5B63;&#xFF0C;&#x6BCF;&#x4E00;&#x96C6;&#x4E4B;&#x524D;&#x5B8C;&#x5168;&#x6C92;&#x6709;&#x9023;&#x8CAB;&#xFF0C;&#x90FD;&#x662F;&#x81EA;&#x6210;&#x4E00;&#x683C;&#x7684;&#x7368;&#x7ACB;&#x6545;&#x4E8B;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#x4F60;&#x53EF;&#x4EE5;&#x96A8;&#x6642;&#x5F9E;&#x96A8;&#x4FBF;&#x4E00;&#x96C6;&#x958B;&#x59CB;&#x770B;&#x90FD;&#x6C92;&#x95DC;&#x4FC2;&#x3002;&#x6BCF;&#x4E00;&#x96C6;&#x90FD;&#x6703;&#x63CF;&#x8FF0;&#x4E00;&#x500B;&#x79D1;&#x6280;&#x6216;&#x5236;&#x5EA6;&#x975E;&#x5E38;&#x6975;&#x7AEF;&#x7684;&#x4E16;&#x754C;&#xFF0C;&#x50CF;&#x662F;&#x80FD;&#x5920;&#x8B93;&#x4EBA;&#x5206;&#x4E0D;&#x6E05;&#x771F;&#x5BE6;&#x6216;&#x865B;&#x64EC;&#x7684; VR&#xFF0C;&#x6216;&#x8005;&#x4E00;&#x500B;&#x80FD;&#x5920;&#x628A;&#x4E00;&#x8F29;&#x5B50;&#x770B;&#x904E;&#x7684;&#x6771;&#x897F;&#x5168;&#x90E8;&#x90FD;&#x9304;&#x4E0B;&#x4F86;&#x7684;&#x96B1;&#x578B;&#x773C;&#x93E1;&#xFF0C;&#x900F;&#x904E;&#x9019;&#x7A2E;&#x6975;&#x7AEF;&#x7684;&#x8A2D;&#x5B9A;&#xFF0C;&#x4F86;&#x8B93;&#x6211;&#x5011;&#x770B;&#x5230;&#x73FE;&#x5728;&#x793E;&#x6703;&#x4E0A;&#x67D0;&#x4E9B;&#x65B9;&#x4FBF;&#x7684;&#x79D1;&#x6280;&#x6216;&#x6CD5;&#x5F8B;&#x53EF;&#x80FD;&#x5E36;&#x4F86;&#x554F;&#x984C;&#x8DDF;&#x96B1;&#x6182;&#x3002;&#x6BCF;&#x4E00;&#x96C6;&#x5927;&#x6982;&#x90FD;&#x4E00;&#x5C0F;&#x6642;&#xFF0C;&#x88FD;&#x4F5C;&#x7CBE;&#x7F8E;&#x7684;&#x50CF;&#x96FB;&#x5F71;&#x4E00;&#x6A23;&#xFF0C;&#x5287;&#x60C5;&#x901A;&#x5E38;&#x4E5F;&#x662F;&#x5B8C;&#x5168;&#x4E0D;&#x6703;&#x6C89;&#x60B6;(&#x9664;&#x4E86;&#x53CD;&#x601D;&#x7684;&#x6642;&#x5019;&#x5E38;&#x6703;&#x89BA;&#x5F97;&#x4EBA;&#x751F;&#x7121;&#x671B;XDDD)&#xFF0C;&#x662F;&#x8A55;&#x50F9;&#x8DDF;&#x95B1;&#x89BD;&#x6578;&#x90FD;&#x76F8;&#x7576;&#x9AD8;&#x7684;&#x5F71;&#x96C6;&#x3002; Men Against Fire &#x662F;&#x7B2C;&#x4E09;&#x5B63;&#x7684;&#x7B2C;&#x4E94;&#x96C6;&#xFF0C;&#x662F;&#x7B2C;&#x4E09;&#x5B63;&#x88E1;&#x9762;&#x500B;&#x4EBA;&#x89BA;&#x5F97;&#x6700;&#x597D;&#x770B;&#x7684;&#x4E00;&#x96C6;(&#x7B2C;&#x516D;&#x96C6;&#x662F;&#x6700;&#x53D7;&#x6B61;&#x8FCE;&#x7684;&#x4E00;&#x96C6;)&#xFF0C;&#x96D6;&#x7136;&#x5F88;&#x60F3;&#x4ECB;&#x7D39;&#x5167;&#x5BB9;&#xFF0C;&#x4F46;&#x662F;&#x4E00;&#x4ECB;&#x7D39;&#x5C31;&#x7206;&#x96F7;&#x4E86;&#xFF0C;&#x6240;&#x4EE5;&#x8ACB;&#x5927;&#x5BB6;&#x5148;&#x53BB;&#x770B;&#x5F71;&#x7247;&#xFF0C;&#x770B;&#x5B8C;&#x518D;&#x56DE;&#x4F86;&#x6572;&#x7897;(&#x4E5F;&#x5C31;&#x662F;&#x62D6;&#x7A3F;)&#x3002;&#x6700;&#x5F8C;&#x63D0;&#x9192;&#xFF0C;&#x5927;&#x5BB6;&#x53EF;&#x4EE5;&#x900F;&#x904E;Netflix&#x89C0;&#x770B;&#x5230;&#x9019;&#x90E8;&#x5F71;&#x96C6;&#x5594;&#xFF0C;&#x4E5F;&#x8ACB; Netflix &#x5982;&#x679C;&#x89BA;&#x5F97;&#x4F60;&#x5011;&#x7684;&#x6D41;&#x91CF;&#x4E0A;&#x5347;&#xFF0C;&#x90A3;&#x61C9;&#x8A72;&#x662F;&#x56E0;&#x70BA;&#x5C0F;&#x5F1F;&#x4E5F;&#x6709;&#x8CA2;&#x737B;&#x4E00;&#x3001;&#x5169;&#x500B;&#x5C0E;&#x6D41;&#xFF0C;&#x5C31;&#x7D66;&#x5C0F;&#x5F1F;&#x4E00;&#x9EDE;&#x696D;&#x914D;&#x8B1D;&#x8B1D;&#xFF01;XD</p>]]></content:encoded></item><item><title><![CDATA[歡迎來到真實世界 - 也是需要來測一下傳說中的MVVM阿]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/11/img_5326.jpg" class="kg-image" alt="IMG_5326.jpg" loading="lazy"></figure><p>&#x5728;<a href="https://koromiko1104.wordpress.com/2017/10/05/mvvmapp/">&#x4E0A;&#x4E00;&#x7BC7;</a>&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x4ECB;&#x7D39;&#x4E86;&#x4E00;&#x500B;&#x65B0;&#x7684;&#x67B6;&#x69CB;&#xFF1A;Model-View-ViewModel(MVVM)&#x3002;&#x900F;&#x904E;MVVM pattern&#xFF0C;&#x6211;&#x5011;&#x628A;business logic&#x8DDF;presentational logic&#x5F9E;ViewController&#x88E1;&#x9762;&#x62BD;&#x51FA;&#x4F86;&#xFF0C;&#x8B8A;&#x6210;&#x4E00;&#x500B;&#x55AE;&#x7D14;</p>]]></description><link>https://huangshihting.works/blog/huan-ying-lai-dao-zhen-shi-shi-jie-ye-shi-xu-yao-lai-ce-yi-xia-chuan-shuo-zhong-de-mvvma/</link><guid isPermaLink="false">61f29f0656cf0e000144184f</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Sun, 12 Nov 2017 15:00:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/11/img_5326.jpg" class="kg-image" alt="IMG_5326.jpg" loading="lazy"></figure><p>&#x5728;<a href="https://koromiko1104.wordpress.com/2017/10/05/mvvmapp/">&#x4E0A;&#x4E00;&#x7BC7;</a>&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x4ECB;&#x7D39;&#x4E86;&#x4E00;&#x500B;&#x65B0;&#x7684;&#x67B6;&#x69CB;&#xFF1A;Model-View-ViewModel(MVVM)&#x3002;&#x900F;&#x904E;MVVM pattern&#xFF0C;&#x6211;&#x5011;&#x628A;business logic&#x8DDF;presentational logic&#x5F9E;ViewController&#x88E1;&#x9762;&#x62BD;&#x51FA;&#x4F86;&#xFF0C;&#x8B8A;&#x6210;&#x4E00;&#x500B;&#x55AE;&#x7D14;&#x597D;&#x6E2C;&#x8A66;&#x7684;&#x7269;&#x4EF6;&#x3002;&#x4F46;&#x662F;&#x5C0D;&#x65BC;&#x5982;&#x4F55;&#x505A;&#x6E2C;&#x8A66;&#xFF0C;&#x537B;&#x662F;&#x652F;&#x5B57;&#x4E0D;&#x63D0;&#xFF0C;&#x4E0D;&#x8981;&#x61F7;&#x7591;&#xFF0C;&#x9019;&#x5C31;&#x662F;&#x62D6;&#x7A3F;(?)&#x3002;&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x88E1;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x8981;&#x4F86;&#x770B;&#x770B;&#xFF0C;&#x600E;&#x6A23;&#x91DD;&#x5C0D;&#x6211;&#x5011;&#x7684;ViewModel&#x4F86;&#x5BEB;Unit Test&#x3002;</p><p>&#x6700;&#x8FD1;APP&#x67B6;&#x69CB;&#x53C8;&#x6210;&#x70BA;&#x5927;&#x5BB6;&#x71B1;&#x9580;&#x7684;&#x8A71;&#x984C;&#xFF0C;&#x6709;&#x5F88;&#x591A;&#x6709;&#x8DA3;&#x7684;&#x6587;&#x7AE0;&#x90FD;&#x5728;&#x91DD;&#x5C0D;&#x9019;&#x7A2E;&#x767E;&#x5BB6;&#x722D;&#x9CF4;&#x7684;iOS app&#x67B6;&#x69CB;&#x73FE;&#x8C61;&#x63D0;&#x51FA;&#x6AA2;&#x8A0E;&#x3002;&#x5176;&#x4E2D;<a href="http://aplus.rs/2017/much-ado-about-ios-app-architecture/?utm_campaign=iOS%2BDev%2BWeekly&amp;utm_medium=email&amp;utm_source=iOS_Dev_Weekly_Issue_326">Much ado about iOS app architecture</a>&#x9019;&#x7BC7;&#x9084;&#x4E0D;&#x932F;&#xFF0C;&#x5927;&#x5BB6;&#x53EF;&#x4EE5;&#x770B;&#x770B;&#x3002;&#x96D6;&#x7136;&#x50CF;&#x662F;MVVM&#x3001;VIPER&#x3001;Clean&#x9019;&#x4E9B;&#x67B6;&#x69CB;&#x7684;&#x76EE;&#x5730;&#x90FD;&#x662F;&#x8981;&#x89E3;&#x6C7A;app&#x67B6;&#x69CB;&#x4E0A;&#x6B0A;&#x8CAC;&#x4E0D;&#x5206;&#x3001;&#x4E0D;&#x6613;&#x6E2C;&#x8A66;&#x7B49;&#x7B49;&#x554F;&#x984C;&#xFF0C;&#x4F46;&#x662F;&#x5F88;&#x5BB9;&#x6613;&#x88AB;&#x7576;&#x6210;Silver Bullet&#xFF0C;&#x4EE5;&#x70BA;&#x53EA;&#x8981;&#x5957;&#x7528;&#x4E86;&#x9019;&#x6A23;&#x7684;&#x5168;&#x65B0;&#x67B6;&#x69CB;&#xFF0C;code&#x5F9E;&#x6B64;&#x5C31;&#x8B8A;&#x5F97;&#x9583;&#x4EAE;&#x4EAE;&#xFF0C;bug&#x4E5F;&#x90FD;&#x81EA;&#x7136;&#x6D88;&#x5931;&#x4E86;&#x3002;&#x53E6;&#x4E00;&#x65B9;&#x9762;&#xFF0C;&#x4E5F;&#x6709;&#x5F88;&#x591A;&#x4EBA;&#x56E0;&#x70BA;&#x9019;&#x4E9B;&#x67B6;&#x69CB;&#x7684;&#x67D0;&#x4E9B;&#x6336;&#x9650;&#xFF0C;&#x800C;&#x5B8C;&#x5168;&#x5426;&#x8A8D;&#x9019;&#x4E9B;&#x67B6;&#x69CB;&#x6240;&#x5E36;&#x4F86;&#x7684;&#x597D;&#x8655;(&#x7C21;&#x55AE;&#x3001;&#x6613;&#x4E0A;&#x624B;&#x7B49;&#x7B49;)&#x3002;</p><p>&#x5728;&#x8EDF;&#x9AD4;&#x7684;&#x4E16;&#x754C;&#xFF0C;&#x771F;&#x7684;&#x6C92;&#x6709;&#x6240;&#x8B02;&#x7684;&#x597D;&#x58DE;&#xFF0C;&#x53EA;&#x6709;&#x9069;&#x4E0D;&#x9069;&#x5408;&#x3002;&#x5C0D;IQ&#x4E0D;&#x9AD8;&#x7684;&#x5C0F;&#x86C7;&#x6211;&#x4F86;&#x8AAA;&#xFF0C;&#x5728;&#x6C92;&#x6709;&#x4EBA;&#x624B;&#x628A;&#x624B;&#x5730;&#x6559;&#x4F60;&#x7684;&#x60C5;&#x6CC1;&#xFF0C;&#x8DDF;&#x672C;&#x5F88;&#x96E3;&#x5728;&#x77ED;&#x6642;&#x9593;&#x9054;&#x5230;&#x9019;&#x4E9B;&#x4EBA;&#x8AAA;&#x7684;MVC&#x597D;&#x68D2;&#x68D2;&#x7684;&#x5883;&#x754C;&#xFF0C;&#x6709;&#x592A;&#x591A;Pattern&#x3001;&#x592A;&#x591A;&#x7684;&#x6CD5;&#x5247;&#x8981;&#x53BB;&#x719F;&#x7DF4;&#xFF0C;&#x66F4;&#x4E0D;&#x7528;&#x8AAA;&#x8981;&#x5F97;&#x5FC3;&#x61C9;&#x624B;&#x5730;&#x61C9;&#x7528;&#x4E86;&#x3002;MVVM&#x7684;&#x597D;&#x8655;&#x662F;&#x5B83;&#x76F8;&#x5C0D;&#x7C21;&#x55AE;&#x5F88;&#x591A;&#xFF0C;&#x4E26;&#x4E14;&#x8981;&#x5F9E;&#x65E2;&#x6709;&#x7684;code&#x6539;&#x5BEB;&#x6210;MVVM&#x4E5F;&#x4E0D;&#x662F;&#x975E;&#x5E38;&#x96E3;&#x7684;&#x4E8B;&#x60C5;&#xFF0C;MVVM&#x5728;&#x5F9E;0&#x958B;&#x59CB;&#x7684;&#x60C5;&#x6CC1;&#x4E0B;&#xFF0C;&#x5C31;&#x975E;&#x5E38;&#x6709;&#x50F9;&#x503C;&#x3002;&#x4F46;&#x5982;&#x679C;&#x5718;&#x968A;&#x795E;&#x4EBA;&#x5F88;&#x591A;&#xFF0C;&#x6709;&#x8001;&#x7DF4;&#x7684;&#x67B6;&#x69CB;&#x5E2B;&#x6216;&#x662F;&#x8CC7;&#x6DF1;&#x5DE5;&#x7A0B;&#x5E2B;&#x5728;&#x5929;&#x5929;&#x5E6B;&#x4F60;&#x770B;code&#xFF0C;MVVM&#x5C31;&#x4E0D;&#x4E00;&#x5B9A;&#x9069;&#x5408;&#x4F60;&#xFF0C;&#x5718;&#x968A;&#x539F;&#x672C;&#x5728;&#x7528;&#x7684;&#x67B6;&#x69CB;&#x8DDF;&#x539F;&#x5247;&#x53CD;&#x800C;&#x624D;&#x6703;&#x662F;&#x6700;&#x9069;&#x5408;&#x7684;&#x3002;</p><p>Btw, &#x70BA;&#x4E86;&#x5BEB;&#x6587;&#x7AE0;&#xFF0C;&#x5C0F;&#x86C7;&#x6211;&#x6BCF;&#x500B;&#x6708;&#x90FD;&#x8981;&#x903C;&#x81EA;&#x5DF1;&#x81F3;&#x5C11;&#x6703;&#x770B;&#x4E00;&#x90E8;&#x96FB;&#x5F71;&#xFF0C;&#x624D;&#x6709;&#x8FA6;&#x6CD5;&#x5BEB;&#x51FA;&#x5FC3;&#x5F97;&#x4F86;(&#x751A;&#x9EBC;&#x7406;&#x7531;)&#x3002;&#x6240;&#x4EE5;&#x60F3;&#x770B;&#x96FB;&#x5F71;&#x63A8;&#x85A6;&#x7684;&#x670B;&#x53CB;&#xFF0C;&#x4E0D;&#x8981;&#x9072;&#x7591;&#xFF0C;&#x76F4;&#x63A5;End&#x5C31;&#x5C0D;&#x4E86;&#xFF01;(&#x4E5F;&#x592A;&#x5FEB;&#x653E;&#x68C4;&#x62B5;&#x6297;)</p><h2 id="tl-dr">TL; DR</h2><p>&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x63D0;&#x5230;&#x5169;&#x500B;&#x6E2C;&#x8A66;&#x7684;&#x5C0F;&#x6280;&#x5DE7;&#xFF1A;</p><ul><li>&#x5982;&#x4F55;&#x8A2D;&#x8A08;mock&#x4F86;&#x6A21;&#x64EC;&#x4E0D;&#x540C;&#x7684;&#x7DB2;&#x8DEF;&#x72C0;&#x6CC1;</li><li>&#x5982;&#x4F55;&#x5229;&#x7528;stub&#x5EFA;&#x7ACB;&#x80FD;&#x5920;&#x88AB;&#x6E2C;&#x8A66;&#x7684;&#x8CC7;&#x6599;&#x72C0;&#x614B;</li></ul><p>&#x5229;&#x7528;&#x9019;&#x5169;&#x500B;&#x6280;&#x5DE7;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x5E6B;&#x6211;&#x5011;&#x7684;ViewModel&#x5EFA;&#x7ACB;&#x5F88;&#x5B8C;&#x6574;&#x7684;&#x6E2C;&#x8A66;&#x3002;</p><h2 id="a-simple-gallery-app">A simple gallery app</h2><p>&#x56DE;&#x9867;&#x4E00;&#x4E0B;&#x4E0A;&#x6B21;&#x6211;&#x5011;&#x7684;<a href="https://koromiko1104.wordpress.com/2017/10/05/mvvmapp/">simple gallery app</a>&#xFF0C;&#x5B83;&#x5177;&#x6709;&#x4EE5;&#x4E0B;&#x7684;&#x529F;&#x80FD;&#xFF1A;</p><ol><li>&#x6703;&#x5F9E;500px API&#x6293;&#x53D6;&#x71B1;&#x9580;&#x76F8;&#x7247;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x76F8;&#x7247;&#x6392;&#x6210;&#x5217;&#x8868;show&#x51FA;&#x4F86;&#xFF0C;&#x6BCF;&#x5F35;&#x76F8;&#x7247;&#x90FD;&#x6703;&#x986F;&#x793A;&#x6A19;&#x984C;&#x3001;&#x63CF;&#x8FF0;&#x3001;&#x8DDF;&#x62CD;&#x651D;&#x65E5;&#x671F;&#x3002;</li><li>&#x5982;&#x679C;&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x9078;&#x4E86;&#x975E;&#x8CE3;&#x54C1;&#xFF0C;app&#x5C31;&#x4E0D;&#x6703;&#x8B93;&#x4F7F;&#x7528;&#x8005;&#x9032;&#x5230;&#x4E0B;&#x4E00;&#x9801;&#xFF0C;&#x4E26;&#x4E14;&#x8DF3;&#x51FA;&#x932F;&#x8AA4;&#x8A0A;&#x606F;&#x3002;</li></ol><p>&#x6211;&#x5011;&#x628A;&#x6700;&#x4E00;&#x958B;&#x59CB;&#x7684;&#x9019;&#x500B;&#x9801;&#x9762;&#x7A31;&#x4F5C;PhotoList&#xFF0C;&#x5B83;&#x7684;&#x4E92;&#x52D5;&#x6D41;&#x7A0B;&#x6703;&#x50CF;&#x4E0B;&#x9762;&#x9019;&#x6A23;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/11/mvvm-001-1.png" class="kg-image" alt="MVVM.001-1.png" loading="lazy"></figure><p>&#x5176;&#x4E2D;APIService&#x8CA0;&#x8CAC;&#x7DB2;&#x8DEF;&#x5C64;&#x7684;&#x6E9D;&#x901A;&#xFF0C;&#x50CF;&#x662F;&#x8A2D;&#x5B9A;URL&#x3001;&#x8A2D;&#x5B9A;request body&#x7B49;&#x7B49;&#x3002;&#x800C;<strong>PhotoListViewModel</strong>&#x5247;&#x6703;&#x8DDF;<strong>APIService</strong>&#x8981;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x8981;&#x5230;&#x7684;&#x8CC7;&#x6599;&#x6574;&#x7406;&#x4E00;&#x4E0B;&#xFF0C;&#x8F49;&#x63DB;&#x6210;&#x80FD;&#x5920;&#x8B93;View&#x7D81;&#x5B9A;&#x7684;&#x5404;&#x7A2E;interfaces&#xFF0C;&#x4E5F;&#x6703;&#x63A5;&#x6536;&#x4F7F;&#x7528;&#x8005;&#x7684;&#x52D5;&#x4F5C;&#xFF0C;&#x505A;&#x51FA;&#x76F8;&#x5C0D;&#x61C9;&#x7684;&#x53CD;&#x61C9;&#x3002;<strong>PhotoListViewController</strong>&#x5C31;&#x662F;&#x55AE;&#x7D14;&#x7684;View&#xFF0C;&#x8CA0;&#x8CAC;&#x5C07;ViewModel&#x7684;&#x8CC7;&#x6599;&#x5728;View&#x4E0A;&#x9762;&#x5448;&#x73FE;&#x51FA;&#x4F86;&#x3002;</p><p>&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x88E1;&#xFF0C;&#x6211;&#x5011;&#x5C07;&#x6703;&#x91DD;&#x5C0D;&#x4E09;&#x500B;&#x4E0D;&#x540C;&#x7684;use cases&#x505A;&#x6E2C;&#x8A66;&#xFF1A;</p><ol><li>&#x8981;&#x80FD;&#x555F;&#x52D5;<strong>APIService</strong>&#x4E0A;&#x7DB2;&#x6293;&#x8CC7;&#x6599;</li><li>&#x7DB2;&#x8DEF;&#x5C64;&#x51FA;&#x932F;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x8981;&#x986F;&#x793A;&#x932F;&#x8AA4;&#x8A0A;&#x606F;</li><li>&#x7576;&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x64CA;for sale&#x7684;&#x7167;&#x7247;&#x6642;&#x8981;&#x5141;&#x8A31;&#x8DF3;&#x5230;&#x4E0B;&#x4E00;&#x9801;</li></ol><h2 id="mvvm-and-dependency-injection">MVVM and Dependency Injection</h2><p>&#x56DE;&#x9867;&#x4E00;&#x4E0B;&#x6211;&#x5011;<strong>PhotoListViewModel</strong>&#x7684;&#x8A2D;&#x8A08;&#xFF1A;</p><p>https://gist.github.com/koromiko/1ef41d1d7d87beac22eaf4cc4a38ec69</p><p>&#x53EF;&#x4EE5;&#x770B;&#x5230;&#xFF0C;<em><strong>apiService</strong></em>&#x9019;&#x500B;&#x7269;&#x4EF6;&#x8CA0;&#x8CAC;&#x8DDF;server&#x62FF;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x5C07;&#x62FF;&#x5230;&#x7684;&#x8CC7;&#x6599;&#x56DE;&#x50B3;&#x7D66;<strong>PhotoListViewModel</strong>&#x4F7F;&#x7528;&#x3002;&#x6211;&#x5011;&#x5229;&#x7528;Dependency Injection(DI)&#x7684;&#x6280;&#x5DE7;&#xFF0C;&#x5C07;&#x8DDF;&#x7DB2;&#x8DEF;&#x5C64;&#x6709;&#x95DC;&#x7684;&#x5DE5;&#x4F5C;&#xFF0C;&#x5168;&#x90E8;&#x90FD;&#x4EA4;&#x7D66;<em><strong>apiService</strong></em>&#x53BB;&#x505A;&#x3002;&#x9019;&#x500B;<em><strong>apiService</strong></em>&#x5728;&#x8DD1;&#x6B63;&#x5F0F;&#x7684;code&#x6642;&#xFF0C;&#x6703;&#x653E;&#x4E0A;&#x771F;&#x7684;<strong>APIService</strong>&#x7269;&#x4EF6;&#xFF0C;&#x8B93;&#x5B83;&#x771F;&#x7684;&#x4E0A;server&#x53BB;&#x6293;&#x8CC7;&#x6599;&#x3002;&#x53E6;&#x4E00;&#x65B9;&#x9762;&#xFF0C;&#x7576;&#x6211;&#x5011;&#x5728;&#x8DD1;&#x6E2C;&#x8A66;&#x6642;&#xFF0C;&#x5C31;&#x7528;&#x88AB;&#x63DB;&#x6210;&#x5047;&#x7684;<strong>MockAPIService</strong>&#x7269;&#x4EF6;&#x3002;&#x9019;&#x6A23;&#x7684;&#x597D;&#x8655;&#x662F;&#x5728;&#x8DD1;&#x6E2C;&#x8A66;&#x6642;&#xFF0C;&#x9664;&#x4E86;&#x53EF;&#x4EE5;&#x4E0D;&#x7528;&#x771F;&#x7684;&#x628A;request&#x6253;&#x4E0A;server&#x4E4B;&#x5916;&#xFF0C;&#x6211;&#x5011;&#x4E5F;&#x53EF;&#x4EE5;&#x5229;&#x7528;<strong>MockAPISerivce</strong>&#x4F86;&#x770B;&#x770B;&#x6211;&#x5011;&#x7684;<strong>PhotoListViewModel</strong>&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x5DE5;&#x4F5C;&#x3002;&#x5B83;&#x5011;&#x4E4B;&#x9593;&#x7684;&#x95DC;&#x4FC2;&#x53EF;&#x4EE5;&#x7528;&#x5E95;&#x4E0B;&#x9019;&#x5F35;&#x5716;&#x4F86;&#x7406;&#x89E3;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/11/mvvm-0011.png" class="kg-image" alt="MVVM.001.png" loading="lazy"></figure><p>&#x5C0D;DI&#x4E0D;&#x719F;&#x7684;&#x8A71;&#xFF0C;&#x5C31;&#x8B93;&#x5C0F;&#x86C7;&#x4F86;&#x696D;&#x914D;&#x4E00;&#x4E0B;(&#x81EA;&#x5DF1;&#x696D;&#x914D;&#x81EA;&#x5DF1;&#xFF1F;)&#xFF0C;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x62D9;&#x4F5C;<a href="https://koromiko1104.wordpress.com/2017/07/30/unit-test-for-networking/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; Unit Test for Networking</a>&#xFF0C;&#x88E1;&#x9762;&#x6709;&#x8A73;&#x7D30;&#x7684;DI&#x6280;&#x5DE7;&#x4ECB;&#x7D39;&#x3002;</p><h2 id="behavior-test">Behavior test</h2><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684;<strong>MockAPIService</strong>&#x9700;&#x8981;&#x6EFF;&#x8DB3;&#x4E0B;&#x5217;&#x5169;&#x500B;&#x9700;&#x6C42;&#xFF1A;</p><ol><li>&#x8981;&#x80FD;&#x78BA;&#x5B9A;<strong>PhotoListViewModel</strong>&#x662F;&#x5426;&#x771F;&#x7684;&#x547C;&#x53EB;&#x4E86;&#x67D0;&#x96BB;function</li><li>&#x8981;&#x80FD;&#x5920;&#x6307;&#x5B9A;&#x4E0D;&#x540C;&#x7684;&#x72C0;&#x614B;&#xFF0C;&#x4F86;&#x6A21;&#x64EC;&#x771F;&#x5BE6;&#x7684;server&#x884C;&#x70BA;</li></ol><p>&#x5148;&#x4F86;&#x770B;&#x9700;&#x6C42;1.&#xFF0C;&#x91DD;&#x5C0D;&#x9700;&#x6C42;1.&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x505A;&#x51FA;&#x9019;&#x6A23;&#x7684;Mock&#xFF1A;</p><p>https://gist.github.com/koromiko/62522e79c2c73cd2c4b92edf96d47e56</p><p>&#x8B93;&#x6211;&#x5011;&#x4F86;&#x597D;&#x597D;&#x770B;&#x4E00;&#x4E0B;&#x9019;&#x500B;Mock&#x3002;&#x9996;&#x5148;&#xFF0C;&#x5B83;&#x7B26;&#x5408;<strong>APIServiceProtocol</strong>&#xFF0C;&#x6240;&#x4EE5;&#x5B83;&#x5B8C;&#x5168;&#x80FD;&#x5920;&#x53D6;&#x4EE3;&#x771F;&#x6B63;&#x7684;<strong>APIService</strong>&#xFF0C;&#x88AB;&#x653E;&#x5230;<strong>PhotoListViewModel</strong>&#x88E1;&#x9762;&#x3002;&#x63A5;&#x8457;&#xFF0C;&#x53EF;&#x4EE5;&#x770B;&#x5230;&#x88E1;&#x9762;&#x6709;&#x500B;property&#xFF1A; <em><strong>isFetchPopularPhotoCalled</strong></em>&#xFF0C;&#x9019;&#x500B;property&#x9810;&#x8A2D;&#x662F;false&#xFF0C;&#x4F46;&#x6703;&#x5728;<strong>APIServiceProtocol.</strong><em>fetchPopularPhoto</em>&#x88AB;&#x547C;&#x53EB;&#x6642;&#x8B8A;&#x6210;true&#xFF0C;&#x9019;&#x500B;&#x8A2D;&#x8A08;&#x7684;&#x7528;&#x610F;&#x5728;&#x65BC;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x900F;&#x904E;&#x9019;&#x500B;<em><strong>isFetchPopularPhotoCalled</strong></em>&#xFF0C;&#x4F86;&#x77E5;&#x9053;<em>fetchPopularPhoto</em>&#x9019;&#x500B;function&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6709;&#x88AB;&#x547C;&#x53EB;&#x5230;&#x3002;</p><p>&#x5229;&#x7528;&#x9019;&#x500B;&#x7C21;&#x55AE;&#x7684;Mock&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x4F86;&#x5BEB;&#x6211;&#x5011;&#x7684;&#x7B2C;&#x4E00;&#x500B;&#x6E2C;&#x8A66;&#xFF1A;</p><p>https://gist.github.com/koromiko/1165c644b0b575b96d7203cfc2fc17b4</p><p>&#x9019;&#x6BB5;&#x7A0B;&#x5F0F;&#x78BC;&#x7FFB;&#x6210;&#x767D;&#x8A71;&#x6587;&#x5C31;&#x662F;&#xFF1A;&#x6211;&#x60F3;&#x8981;&#x77E5;&#x9053;&#xFF0C;&#x5728;<em>sut.initFetch()</em>&#x4E4B;&#x5F8C;&#xFF0C;<strong>APIServiceProtocol</strong>. <em>fetchPopularPhoto</em>&#x662F;&#x5426;&#x6709;&#x88AB;&#x78BA;&#x5BE6;&#x5730;&#x57F7;&#x884C;&#x3002;&#x5229;&#x7528;&#x9019;&#x500B;&#x6280;&#x5DE7;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x6E2C;&#x8A66;ViewModel&#x8DDF;&#x5B83;&#x7684;dependency objects&#x4E4B;&#x9593;&#x7684;&#x4E92;&#x52D5;&#x4E86;&#x3002;</p><h2 id="success-or-failure">Success or Failure?</h2><p>&#x9664;&#x4E86;&#x6E2C;&#x8A66;&#x6211;&#x5011;&#x7684;ViewModel&#x662F;&#x4E0D;&#x662F;&#x6709;&#x78BA;&#x5BE6;&#x547C;&#x53EB;<em>fetchPopularPhoto</em>&#x4E4B;&#x5916;&#xFF0C;&#x66F4;&#x91CD;&#x8981;&#x7684;&#x662F;&#xFF0C;&#x6211;&#x5011;&#x60F3;&#x77E5;&#x9053;&#x7576;api request&#x6210;&#x529F;&#x6216;&#x5931;&#x6557;&#x6642;&#xFF0C;&#x6211;&#x5011;&#x7684;<strong>PhotoListViewModel</strong>&#x662F;&#x4E0D;&#x662F;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x8655;&#x7406;&#x9019;&#x4E9B;&#x72C0;&#x6CC1;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x7B2C;&#x4E8C;&#x500B;&#x9700;&#x6C42;&#xFF1A;mock&#x8981;&#x80FD;&#x5920;&#x6307;&#x5B9A;&#x4E0D;&#x540C;&#x7684;&#x72C0;&#x614B;&#xFF0C;&#x4F86;&#x6A21;&#x64EC;&#x771F;&#x5BE6;&#x7684;server&#x884C;&#x70BA;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;<strong>MockAPIService</strong>&#x9700;&#x8981;&#x80FD;&#x5920;&#x807D;&#x5F9E;&#x6211;&#x5011;&#x7684;&#x6307;&#x4EE4;&#xFF0C;&#x7576;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x5B83;&#x6210;&#x529F;&#xFF0C;&#x5B83;&#x5C31;&#x8981;&#x6210;&#x529F;&#xFF0C;&#x7576;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x5B83;&#x5931;&#x6557;&#xFF0C;&#x5B83;&#x5C31;&#x8981;&#x4E56;&#x4E56;&#x5730;&#x5931;&#x6557;&#xFF0C;&#x9019;&#x5C31;&#x662F;&#x4EBA;&#x5728;&#x5C4B;&#x7C37;&#x4E0B;&#xFF0C;&#x4E0D;&#x5F97;&#x4E0D;&#x4F4E;&#x982D;(&#x53EF;&#x4EE5;&#x9019;&#x6A23;&#x96A8;&#x4FBF;&#x4E82;&#x7528;&#xFF1F;)&#x3002;</p><p>&#x5728;&#x9019;&#x88E1;&#xFF0C;&#x6211;&#x5011;&#x5148;&#x5F9E;test code&#x958B;&#x59CB;&#x770B;&#x8D77;&#x3002;&#x525B;&#x525B;&#x6211;&#x5011;&#x6709;&#x500B;use case&#x662F;&#x9019;&#x6A23;&#x7684;&#xFF1A;</p><ol><li>&#x7DB2;&#x8DEF;&#x5C64;&#x51FA;&#x932F;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x8981;&#x986F;&#x793A;&#x932F;&#x8AA4;&#x8A0A;&#x606F;</li></ol><p>&#x6839;&#x64DA;&#x9019;&#x500B;case&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x5BEB;&#x51FA;&#x9019;&#x6A23;&#x7684;&#x6E2C;&#x8A66;code&#xFF1A;</p><p>https://gist.github.com/koromiko/37d772b81c62bae16c82bb13b9ed0f7a</p><p>&#x6211;&#x5011;&#x6703;&#x5148;&#x89F8;&#x767C;<em>initFetch</em>&#xFF0C;&#x8B93;<strong>PhotoListViewModel</strong>&#x900F;&#x904E;<strong>APIServiceProtocol</strong>&#x4E0A;&#x7DB2;&#x53BB;&#x6293;&#x8CC7;&#x6599;&#x3002;&#x7136;&#x5F8C;&#x5728;<em><strong>mockAPIService</strong></em>.<em>fetchFail(error: error)</em>&#x9019;&#x88E1;&#xFF0C;&#x6211;&#x5011;&#x5C07;<em><strong>mockAPIService</strong></em>&#x8A2D;&#x5B9A;&#x6210;&#x4E00;&#x5B9A;&#x6703;&#x56DE;&#x50B3;&#x5931;&#x6557;&#xFF0C;&#x4E26;&#x4E14;&#x6307;&#x5B9A;&#x597D;&#x932F;&#x8AA4;&#x7684;&#x985E;&#x578B;&#x3002;&#x6700;&#x5F8C;&#x6211;&#x5011;&#x6703;&#x9A57;&#x8B49;<strong>PhotoListViewModel</strong>&#x662F;&#x5426;&#x6709;&#x8A2D;&#x5B9A;&#x597D;&#x5C0D;&#x61C9;&#x7684;&#x932F;&#x8AA4;&#x8A0A;&#x606F;&#x3002;&#x9019;&#x500B;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x6307;&#x5B9A;&#x932F;&#x8AA4;&#x985E;&#x578B;&#x7684;mock&#x8B93;&#x4F60;&#x53EF;&#x4EE5;&#x5F88;&#x8F15;&#x6613;&#x5730;&#x6A21;&#x64EC;&#x5404;&#x7A2E;&#x6B63;&#x78BA;&#x6216;&#x932F;&#x8AA4;&#x60C5;&#x6CC1;&#xFF0C;&#x4E26;&#x4E14;&#x770B;&#x770B;&#x4F60;&#x7684;&#x7269;&#x4EF6;&#x662F;&#x4E0D;&#x662F;&#x6B63;&#x5E38;&#x529F;&#x80FD;&#x4E2D;&#xFF0C;&#x662F;&#x4E0D;&#x662F;&#x5F88;&#x65B9;&#x4FBF;&#x5462;&#xFF1F;(&#x662F;)</p><p>&#x63A5;&#x8457;&#x6211;&#x5011;&#x4F86;&#x770B;&#x770B;&#x9019;&#x6A23;&#x7684;mock&#x8981;&#x600E;&#x9EBC;&#x8A2D;&#x8A08;&#x3002;&#x5148;&#x4F86;&#x56DE;&#x9867;&#x4E00;&#x4E0B;&#x6211;&#x5011;&#x7684;<strong>PhotoListViewModel</strong>.<em>initFetch()</em>&#xFF1A;</p><p>https://gist.github.com/koromiko/26bd480bbaec34a5a5736a9a05318aec</p><p><em>initFetch</em>&#x6703;&#x5148;&#x555F;&#x52D5;<em><strong>apiService</strong></em>&#x7684;<em>fetchPopularPhoto</em>&#xFF0C;&#x4E26;&#x4E14;&#x8A2D;&#x5B9A;&#x597D;callback closure&#xFF0C;&#x7B49;&#x5F85;<strong><em>apiService</em></strong>&#x5B8C;&#x6210;&#x5DE5;&#x4F5C;&#x547C;&#x53EB;callback closure&#xFF0C;&#x518D;&#x505A;&#x5C0D;&#x61C9;&#x7684;&#x8655;&#x7406;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5982;&#x679C;&#x5728;MockAPIService&#x8981;&#x6A21;&#x64EC;&#x5931;&#x6557;&#x7684;API request&#xFF0C;&#x5C31;&#x8981;&#x5F9E;&#x9019;&#x500B;callback closure&#x4E0B;&#x624B;&#xFF01;</p><p>&#x6700;&#x5F8C;&#x6211;&#x5011;&#x5C31;&#x5BE6;&#x4F5C;&#x51FA;&#x4E86;&#x9019;&#x6A23;&#x7684;<strong>MockAPIService</strong>&#xFF1A;</p><p>https://gist.github.com/koromiko/ddce82f819d5ea941f99f25265af5788</p><p>&#x5F9E;&#x4E0A;&#x9762;&#x7684;&#x7A0B;&#x5F0F;&#x78BC;&#x53EF;&#x4EE5;&#x770B;&#x5230;&#xFF0C;&#x7576;<strong>MockAPIService</strong>.<em>fetchPopularPhoto</em>&#x88AB;&#x547C;&#x53EB;&#x6642;&#xFF0C;&#x6703;&#x5148;&#x628A;callback closure&#x5B58;&#x4E0B;&#x4F86;:</p><pre><code>completeClosure = complete</code></pre><p>&#x7B49;&#x5230;<strong>MockAPIService</strong>.<em>fetchSuccess</em>&#x6216;<strong>MockAPIService</strong>.<em>fetchFail</em>&#x88AB;&#x547C;&#x53EB;&#x6642;&#xFF0C;&#x624D;&#x6703;&#x89F8;&#x767C;callback&#x3002;&#x5728;&#x6211;&#x5011;&#x9084;&#x6C92;&#x547C;&#x53EB;<em>fetchSuccess</em>&#x6216;<em>fetchFail</em>&#x4E4B;&#x524D;&#xFF0C;<strong>PhotoListViewModel</strong>&#x7684;callback&#x662F;&#x4E0D;&#x6703;&#x88AB;&#x547C;&#x53EB;&#x7684;&#x3002;&#x9019;&#x6A23;&#x5C31;&#x5B8C;&#x6210;&#x4E86;&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;&#x975E;&#x540C;&#x6B65;&#x3001;&#x4E0D;&#x540C;&#x72C0;&#x614B;&#x7684;&#x6A21;&#x64EC;&#x3002;&#x9019;&#x6A23;&#x7684;&#x6548;&#x679C;&#x5C31;&#x5982;&#x540C;&#x4E0A;&#x9762;&#x7684;test code&#x4E00;&#x6A23;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x900F;&#x904E;&#x547C;&#x53EB;fetchFail&#x4E26;&#x6307;&#x5B9A;error object&#x4F86;&#x6A21;&#x64EC;api request&#x5931;&#x6557;&#x7684;&#x72C0;&#x6CC1;&#x3002;</p><h2 id="stubs-for-viewmodel">Stubs for ViewModel</h2><p>&#x6211;&#x5011;&#x7684;ViewModel&#xFF0C;&#x9664;&#x4E86;&#x63D0;&#x4F9B;&#x5404;&#x7A2E;properties&#x8B93;View&#x4F5C;&#x8CC7;&#x6599;&#x7684;&#x7D81;&#x5B9A;&#x4E4B;&#x5916;&#xFF0C;&#x4E5F;&#x63D0;&#x4F9B;&#x63A5;&#x53E3;&#x8B93;View&#x80FD;&#x5920;&#x628A;&#x4F7F;&#x7528;&#x8005;&#x7684;&#x884C;&#x70BA;&#x50B3;&#x56DE;&#x4F86;&#xFF0C;&#x4E26;&#x4E14;&#x505A;&#x51FA;&#x76F8;&#x5C0D;&#x61C9;&#x7684;&#x6539;&#x8B8A;&#xFF0C;&#x4F86;&#x8B93;View&#x7522;&#x751F;&#x8B8A;&#x5316;&#x3002;&#x9019;&#x6A23;&#x7684;&#x884C;&#x70BA;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x600E;&#x6A23;&#x505A;&#x6E2C;&#x8A66;&#x5462;&#xFF1F;&#x4E00;&#x6A23;&#xFF0C;&#x6211;&#x5011;&#x5148;&#x5F9E;use case&#x958B;&#x59CB;&#x770B;&#x8D77;&#xFF1A;</p><ol><li>&#x7576;&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x64CA;for sale&#x7684;&#x7167;&#x7247;&#x6642;&#x8981;&#x5141;&#x8A31;&#x8DF3;&#x5230;&#x4E0B;&#x4E00;&#x9801;</li></ol><p>&#x4F9D;&#x7167;&#x4E0A;&#x9762;&#x7684;use case&#xFF0C;&#x6211;&#x5011;&#x5BEB;&#x4E86;&#x4EE5;&#x4E0B;&#x7684;test code&#xFF1A;</p><p>https://gist.github.com/koromiko/d5fa15c50eb3f3627cc6389202eb109f</p><p>&#x9019;&#x6BB5;test code&#x4EE3;&#x8868;&#x7684;&#x610F;&#x601D;&#x662F;&#xFF0C;&#x7576;&#x4F7F;&#x7528;&#x8005;&#x6309;&#x4E0B;&#x7B2C;&#x4E00;&#x500B;cell&#x6642;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x6E2C;&#x8A66;<em><strong>allowSegue</strong></em>&#x662F;&#x5426;&#x70BA;true&#x3002;&#x9019;&#x6BB5;code&#x6709;&#x5169;&#x500B;&#x554F;&#x984C;&#xFF1A;</p><ul><li>ViewModel&#x5728;&#x9084;&#x6C92;<em>initFetch</em>&#x4E4B;&#x524D;&#x90FD;&#x4E0D;&#x6703;&#x6709;&#x8CC7;&#x6599;&#xFF0C;&#x6240;&#x4EE5;&#x9019;&#x6A23;&#x6703;&#x89F8;&#x767C;exception</li><li>&#x6211;&#x5011;&#x9810;&#x8A2D;&#x4E86;IndexPath(row: 0, section: 0)&#x7684;photo&#x662F;for sale&#x4E86;&#xFF0C;&#x4F46;&#x4E8B;&#x5BE6;&#x4E0A;&#x5B83;&#x662F;&#x7A7A;&#x7684;</li></ul><p>&#x9019;&#x6642;&#x5019;&#xFF0C;stubs&#x5C31;&#x53EF;&#x4EE5;&#x6D3E;&#x4E0A;&#x7528;&#x5834;&#x4E86;&#xFF01;Stubs&#x5728;&#x6E2C;&#x8A66;&#x7684;&#x8A2D;&#x8A08;&#x4E0A;&#x4EE3;&#x8868;&#x7684;&#x662F;&#x4E00;&#x4E9B;&#x9810;&#x5148;&#x6E96;&#x5099;&#x597D;&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x8A73;&#x7D30;&#x7684;&#x5B9A;&#x7FA9;&#x53EF;&#x4EE5;&#x518D;&#x53C3;&#x8003;<a href="https://koromiko1104.wordpress.com/2017/10/05/unit-test-for-core-data/">&#x62D9;&#x4F5C;</a>(&#x7121;&#x5B54;&#x4E0D;&#x5165;&#x5427;&#xFF01;)&#x3002;&#x70BA;&#x4E86;&#x89E3;&#x6C7A;&#x6C92;&#x6709;&#x8CC7;&#x6599;&#xFF0C;&#x4F46;&#x662F;&#x53C8;&#x4E0D;&#x80FD;&#x771F;&#x7684;&#x9023;&#x4E0A;server&#x53BB;&#x53D6;&#x8CC7;&#x6599;&#x7684;&#x72C0;&#x6CC1;&#xFF0C;&#x6211;&#x5011;&#x5FC5;&#x9808;&#x8981;&#x8A2D;&#x8A08;&#x4E00;&#x4E9B;stubs&#x4F86;&#x9A19;&#x904E;&#x6211;&#x5011;&#x7684;<strong>PhotoListViewModel</strong>&#x3002;&#x6240;&#x4EE5;&#x73FE;&#x5728;&#x4F86;&#x5E6B;&#x6211;&#x5011;&#x7684;<strong>MockAPIService</strong>&#x505A;&#x9EDE;&#x4FEE;&#x6539;&#xFF0C;&#x8B93;&#x5B83;&#x53EF;&#x4EE5;&#x56DE;&#x50B3;&#x8A2D;&#x8A08;&#x597D;&#x7684;stubs&#xFF1A;</p><p>https://gist.github.com/koromiko/c619cbacb3d586e7dffe59277563cf20</p><p>&#x5176;&#x4E2D;<em><strong>completePhotos</strong></em>&#x9019;&#x500B;property&#x5C31;&#x662F;&#x6211;&#x5011;&#x653E;&#x7F6E;stubs&#x7684;&#x5730;&#x65B9;&#xFF0C;&#x53EA;&#x8981;&#x5728;&#x9019;&#x908A;&#x6307;&#x5B9A;&#x597D;Photo objects&#xFF0C;&#x5728;&#x547C;&#x53EB;<strong>MockAPIService</strong>.<em>fetchSuccess</em>&#x6642;&#xFF0C;<em><strong>completeClosure</strong></em>&#x5C31;&#x6703;&#x628A;<em><strong>completePhotos</strong></em>&#x88E1;&#x9762;&#x7684;&#x5167;&#x5BB9;&#x56DE;&#x50B3;&#x7D66;<strong>PhotoListViewModel</strong>&#x3002;&#x518D;&#x56DE;&#x5230;&#x6211;&#x5011;&#x7684;test code&#xFF0C;&#x6211;&#x5011;&#x73FE;&#x5728;&#x628A;test code&#x4FEE;&#x6539;&#x6210;&#x9019;&#x6A23;&#xFF1A;</p><p>https://gist.github.com/koromiko/1e145c3409b158f4fea4a174a789f64b</p><p>&#x6211;&#x5011;&#x5148;&#x628A;&#x6E96;&#x5099;&#x597D;&#x7684;Stubs&#xFF1A;<strong>StubGenerator()</strong>.<em>stubPhotos()</em>, &#x4E1F;&#x5230;<em><strong>mockAPIService</strong></em>.<em>completePhotos</em>&#x88E1;&#x9762;&#xFF0C;&#x63A5;&#x8457;&#x89F8;&#x767C;<strong>PhotoListViewModel</strong>&#x7684;<em>initFetch</em>&#xFF0C;&#x9019;&#x6642;&#x5019;<strong>PhotoListViewModel</strong>&#x6703;&#x53BB;&#x8DDF;<em><strong>mockAPIService</strong></em>&#x8981;&#x8CC7;&#x6599;&#xFF0C;&#x7136;&#x5F8C;&#x6211;&#x5011;&#x518D;&#x900F;&#x904E;&#x547C;&#x53EB;<em><strong>mockAPIService</strong></em>.<em>fetchSuccess</em>()&#xFF0C;&#x4F86;&#x8B93;stubs&#x56DE;&#x50B3;&#x7D66;<strong>PhotoListViewModel</strong>&#xFF0C;&#x5B8C;&#x6210;&#x8CC7;&#x6599;&#x7684;&#x6E96;&#x5099;&#x3002;</p><p>&#x9019;&#x6A23;&#x4E00;&#x4F86;&#xFF0C;&#x6211;&#x5011;&#x5728;test code&#x88E1;&#x9762;&#x547C;&#x53EB;<em><strong>sut</strong>.userPressed(</em>at:<em> indexPath)</em>&#x5C31;&#x6C92;&#x6709;&#x554F;&#x984C;&#x4E86;&#xFF0C;&#x56E0;&#x70BA;&#x9019;&#x6642;&#x5019;<strong>PhotoListViewModel</strong>&#x7684;&#x72C0;&#x614B;&#x5C31;&#x6703;&#x662F;&#x5DF2;&#x7D93;&#x622A;&#x53D6;&#x5B8C;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x56E0;&#x70BA;stubs&#x662F;&#x6211;&#x5011;&#x5728;&#x6E2C;&#x8A66;&#x6642;&#x4E00;&#x4F75;&#x653E;&#x9032;&#x53BB;&#x7684;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4E5F;&#x77E5;&#x9053;&#x6211;&#x5011;&#x6B63;&#x5728;&#x6E2C;&#x8A66;&#x7684;photo&#x662F;&#x4E0D;&#x662F;for sale&#x4E86;&#x3002;</p><h2 id="more-tests-and-more-todos">More tests and more todos</h2><p>&#x9019;&#x500B;&#x5C0F;app&#x9084;&#x6709;&#x66F4;&#x591A;&#x7684;&#x6E2C;&#x8A66;&#xFF0C;&#x6709;&#x8208;&#x8DA3;&#x7684;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x5C0F;&#x5F1F;&#x7684;&#x539F;&#x59CB;&#x78BC;&#xFF1A;</p><p><a href="https://github.com/koromiko/Tutorial/tree/master/MVVMPlayground">Tutorial/MVVMPlayground at master &#xB7; koromiko/Tutorial &#xB7; GitHub</a></p><p>&#x88E1;&#x9762;&#x5305;&#x542B;&#x4E86;&#x9019;&#x4E9B;test case&#xFF1A;</p><ol><li>&#x8981;&#x80FD;&#x555F;&#x52D5;APIService&#x4E0A;&#x7DB2;&#x6293;&#x8CC7;&#x6599;</li><li>&#x6293;&#x8CC7;&#x6599;&#x7684;&#x6642;&#x5019;&#x8981;&#x6B63;&#x78BA;&#x986F;&#x793A;&#x8B80;&#x53D6;&#x52D5;&#x756B;</li><li>&#x7DB2;&#x8DEF;&#x5C64;&#x51FA;&#x932F;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x8981;&#x986F;&#x793A;&#x932F;&#x8AA4;&#x8A0A;&#x606F;</li><li>cell&#x6578;&#x91CF;&#x8981;&#x6B63;&#x78BA;</li><li>cell&#x5167;&#x5BB9;&#x8981;&#x6B63;&#x78BA;</li><li>&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x64CA;for sale&#x7684;&#x7167;&#x7247;&#x6642;&#x8981;&#x5141;&#x8A31;&#x8DF3;&#x5230;&#x4E0B;&#x4E00;&#x9801;</li><li>&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x64CA;not for sale&#x7684;&#x7167;&#x7247;&#x6642;&#x4E0D;&#x80FD;&#x6709;&#x52D5;&#x4F5C;&#x4E26;&#x4E14;&#x8981;&#x986F;&#x793A;&#x932F;&#x8AA4;&#x8A0A;&#x606F;</li></ol><p>&#x53EF;&#x4EE5;&#x770B;&#x5230;&#xFF0C;&#x5728;MVVM&#x7684;&#x67B6;&#x69CB;&#x5E95;&#x4E0B;&#xFF0C;&#x6211;&#x5011;&#x5BEB;&#x7684;&#x6E2C;&#x8A66;&#x53EF;&#x4EE5;&#x5E7E;&#x4E4E;&#x6DB5;&#x84CB;&#x6574;&#x500B;&#x6A21;&#x7D44;&#xFF0C;&#x5305;&#x62EC;presentational logic&#x9084;&#x6709;&#x5404;&#x7A2E;&#x8907;&#x96DC;&#x7684;state&#x90FD;&#x80FD;&#x5920;&#x88AB;&#x6E2C;&#x8A66;&#x5230;&#x3002;&#x9019;&#x5C31;&#x662F;MVVM&#x7684;&#x597D;&#x8655;&#xFF0C;&#x5B83;&#x5BB9;&#x6613;&#x4E0A;&#x624B;&#x4E26;&#x4E14;&#x4E0D;&#x6703;&#x6709;&#x592A;&#x591A;&#x7684;boilerplate&#x3002;</p><p>&#x76F8;&#x5C0D;&#x7684;&#xFF0C;&#x6211;&#x5011;&#x7684;&#x9019;&#x500B;&#x6E2C;&#x8A66;&#x5C0F;app&#x4E5F;&#x6709;&#x5F88;&#x591A;&#x5F85;&#x6539;&#x5584;&#x7684;&#x9EDE;&#xFF0C;&#x50CF;&#x662F;View&#x9019;&#x4E00;&#x5C64;&#x5B8C;&#x5168;&#x6C92;&#x6E2C;&#x8A66;&#xFF0C;ViewModel&#x7684;&#x5DE5;&#x4F5C;&#x592A;&#x591A;&#xFF0C;&#x9084;&#x6709;&#x95DC;&#x65BC;ViewModel&#x5012;&#x5E95;&#x61C9;&#x8A72;stateless&#x9084;&#x662F;&#x8981;&#x6709;&#x5B8C;&#x6574;&#x7684;state&#x4EE5;&#x65B9;&#x4FBF;&#x6E2C;&#x8A66;&#xFF0C;&#x9019;&#x4E9B;&#x9EDE;&#x90FD;&#x662F;&#x672A;&#x4F86;&#x53EF;&#x4EE5;&#x6539;&#x9032;&#x7684;&#x76EE;&#x6A19;&#x3002;&#x9019;&#x500B;&#x7CFB;&#x5217;&#x672A;&#x4F86;&#x6703;&#x4E00;&#x6B65;&#x4E00;&#x6B65;&#x5730;refactor&#x9019;&#x500B;&#x5C0F;app&#xFF0C;&#x6B61;&#x8FCE;&#x8A02;&#x95B1;&#x5C0F;&#x86C7;&#x7684;blog&#xFF0C;&#x4E00;&#x8D77;&#x4F86;&#x7814;&#x7A76;&#x600E;&#x6A23;&#x5BEB;&#x51FA;&#x66F4;&#x68D2;&#x7684;app&#x5427;&#xFF01;</p><h2 id="recap">Recap</h2><p>&#x5728;&#x9019;&#x500B;&#x7C21;&#x55AE;&#x7684;&#x5206;&#x4EAB;&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x900F;&#x904E;&#x8A2D;&#x8A08;&#x597D;&#x7684;<strong>MockAPIService</strong>&#xFF0C;&#x4F86;&#x6A21;&#x64EC;&#x5404;&#x7A2E;&#x73FE;&#x5BE6;&#x751F;&#x6D3B;&#x4E2D;&#x6703;&#x767C;&#x751F;&#x7684;&#x60C5;&#x5F62;&#xFF0C;&#x4E26;&#x4E14;&#x8B93;&#x6E2C;&#x8A66;&#x7684;code&#x80FD;&#x5920;&#x5B8C;&#x6574;&#x6DB5;&#x84CB;&#x5404;&#x7A2E;&#x72C0;&#x6CC1;&#x3002;&#x9019;&#x500B;<strong>MockAPIService</strong>&#x7684;&#x4EFB;&#x52D9;&#x4E3B;&#x8981;&#x6709;&#xFF1A;</p><ul><li>&#x8A18;&#x9304;SUT&#x662F;&#x5426;&#x6709;&#x78BA;&#x5BE6;&#x8207;&#x5B83;&#x4E92;&#x52D5;</li><li>&#x8A18;&#x9304;SUT&#x50B3;&#x9032;&#x53BB;&#x7684;&#x8CC7;&#x6599;&#x662F;&#x5426;&#x6B63;&#x78BA;</li><li>&#x6A21;&#x64EC;&#x5404;&#x7A2E;&#x4E0D;&#x540C;&#x7684;&#x72C0;&#x614B;</li></ul><p>&#x900F;&#x904E;&#x9019;&#x500B;Mock&#xFF0C;&#x52A0;&#x4E0A;MVVM&#x628A;presentational logic&#x5F9E;ViewController&#x88E1;&#x9762;&#x62C6;&#x5206;&#x51FA;&#x4F86;&#x7684;&#x7279;&#x6027;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x6210;&#x529F;&#x5730;&#x5B8C;&#x6210;&#x6240;&#x6709;use case&#x7684;&#x6E2C;&#x8A66;&#x4E86;&#xFF01;</p><p>&#x9084;&#x662F;&#x8981;&#x5F37;&#x8ABF;&#xFF0C;&#x6C92;&#x6709;silver bullet&#xFF0C;&#x5230;&#x9019;&#x908A;&#x53EA;&#x662F;&#x4E00;&#x958B;&#x59CB;&#x800C;&#x5DF2;&#xFF0C;&#x5C0F;&#x86C7;&#x6211;&#x4E5F;&#x9084;&#x5728;&#x5B78;&#x7FD2;&#x7576;&#x4E2D;&#xFF01;&#x6B61;&#x8FCE;&#x5927;&#x5927;&#x5011;&#x7D66;&#x4E88;&#x5404;&#x7A2E;&#x5EFA;&#x8B70;&#xFF0C;&#x4E5F;&#x6B61;&#x8FCE;&#x52A0;&#x5165;&#x8A0E;&#x8AD6;&#xFF0C;&#x89BA;&#x5F97;&#x90A3;&#x908A;&#x89C0;&#x5FF5;&#x6709;&#x932F;&#x6216;&#x662F;&#x7A0B;&#x5F0F;&#x6709;&#x932F;&#x4E5F;&#x6B61;&#x8FCE;&#x63D0;&#x51FA;&#x4F86;&#x5594;&#xFF01;</p><p>&#x6211;&#x7684;FB&#x90FD;&#x5728;&#x5587;&#x8CFD;XD&#xFF0C;&#x6240;&#x4EE5;&#x60F3;&#x770B;&#x6280;&#x8853;&#x76F8;&#x95DC;&#x7684;&#xFF0C;&#x8ACB;follow&#x5C0F;&#x86C7;&#x7684;Twitter: <a href="https://twitter.com/KoromikoNeo">https://twitter.com/KoromikoNeo</a></p><hr><p>&#x6700;&#x5F8C;&#x9032;&#x5165;&#x672C;&#x6587;XD</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/11/blade_runner.jpg" class="kg-image" alt="blade_runner.jpg" loading="lazy"></figure><p>&#x5F9E;&#x5F88;&#x5C0F;&#x7684;&#x6642;&#x5019;&#x958B;&#x59CB;&#xFF0C;&#x9019;&#x500B;&#x756B;&#x9762;&#x5C31;&#x662F;&#x5FC3;&#x4E2D;&#x672A;&#x4F86;&#x4E16;&#x754C;&#x7684;&#x4EE3;&#x8868;&#xFF0C;&#x5982;&#x679C;&#x4F60;&#x554F;&#x6211;&#x672A;&#x4F86;&#x4E16;&#x754C;&#x9577;&#x600E;&#x6A23;&#xFF0C;&#x6211;&#x5C31;&#x6703;&#x7167;&#x8457;&#x9019;&#x500B;&#x5834;&#x666F;&#x63CF;&#x8FF0;&#x7D66;&#x4F60;&#x807D;&#x3002;&#x5230;&#x73FE;&#x5728;&#x6211;&#x9084;&#x662F;&#x80FD;&#x5920;&#x56DE;&#x60F3;&#x7B2C;&#x4E00;&#x6B21;&#x770B;&#x5230;&#x9019;&#x500B;&#x5834;&#x666F;&#x8A2D;&#x5B9A;&#x7684;&#x611F;&#x52D5;&#xFF0C;&#x4E5F;&#x56E0;&#x70BA;&#x9019;&#x90E8;&#x96FB;&#x5F71;&#x8B93;&#x6211;&#x958B;&#x59CB;&#x559C;&#x6B61;cyberpuck&#xFF0C;&#x89BA;&#x5F97;&#x90A3;&#x7A2E;&#x7528;&#x820A;&#x6280;&#x8853;&#x9293;&#x91CB;&#x7684;&#x672A;&#x4F86;&#x5341;&#x5206;&#x8FF7;&#x4EBA;&#x3002;</p><p>&#x9019;&#x90E8;&#x5C31;&#x662F;1982&#x5E74;&#x7684;Blade Runner(&#x9280;&#x7FFC;&#x6BBA;&#x624B;)</p><p>&#x4E0D;&#x904E;&#x9019;&#x90E8;&#x7247;&#x5728;&#x5C0D;&#x8A71;&#x3001;&#x6558;&#x4E8B;&#x3001;&#x9084;&#x6709;&#x908F;&#x8F2F;&#x7684;&#x8655;&#x7406;&#x771F;&#x7684;&#x6709;&#x5F85;&#x52A0;&#x5F37;&#xFF0C;bug&#x8D85;&#x591A;&#xFF0C;&#x89D2;&#x8272;&#x523B;&#x756B;&#x4E0D;&#x6DF1;&#xFF0C;&#x9084;&#x6709;&#x5F88;&#x591A;&#x5982;&#x679C;&#x6C92;&#x6709;&#x5F8C;&#x4EBA;&#x7684;&#x89E3;&#x91CB;&#x8DDF;&#x672C;&#x9023;&#x60F3;&#x50CF;&#x7A7A;&#x9593;&#x90FD;&#x6C92;&#x6709;&#x7684;&#x6666;&#x6F80;&#x5C0D;&#x767D;&#xFF0C;&#x76F8;&#x8F03;&#x4E4B;&#x4E0B;&#x66F4;&#x55AE;&#x8ABF;&#x7684;2001 Space Odessey&#x53CD;&#x800C;&#x5728;&#x8AAA;&#x6545;&#x4E8B;&#x65B9;&#x9762;&#x7565;&#x52DD;n&#x7C4C;XD</p><p>&#x6240;&#x4EE5;&#x5C0F;&#x86C7;&#x771F;&#x6B63;&#x63A8;&#x85A6;&#x7684;&#x662F;Blade Runner 2049 XD</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/11/blade3.jpg" class="kg-image" alt="blade3.jpg" loading="lazy"></figure><p>&#x5B8C;&#x5168;&#x53EF;&#x4EE5;&#x6490;&#x8D77;&#x539F;&#x4F5C;&#x751A;&#x81F3;&#x4EE5;&#x8AAA;&#x6545;&#x4E8B;&#x7684;&#x80FD;&#x529B;&#x4F86;&#x8AAA;&#x9084;&#x6BD4;&#x524D;&#x4F5C;&#x5F37;&#x5927;&#xFF0C;&#x96D6;&#x7136;&#x4EE5;&#x73FE;&#x4EE3;&#x7684;&#x773C;&#x5149;&#x4F86;&#x770B;&#x9019;&#x7A2E;&#x5230;&#x8655;&#x90FD;&#x662F;&#x523B;&#x610F;&#x96D5;&#x7422;&#x7684;&#x756B;&#x9762;&#x53CA;&#x5287;&#x672C;&#xFF0C;&#x9084;&#x6709;&#x5DF2;&#x7D93;&#x4E0D;&#x7B97;&#x524D;&#x885B;&#x5F71;&#x50CF;&#x98A8;&#x683C;&#x53EF;&#x80FD;&#x5DF2;&#x7D93;&#x4E0D;&#x90A3;&#x9EBC;&#x5403;&#x9999;&#xFF0C;&#x4F46;&#x662F;&#x56E0;&#x70BA;&#x5B83;&#x662F;Blade Runner&#x7684;&#x7E8C;&#x4F5C;&#xFF0C;&#x6240;&#x4EE5;&#x770B;&#x8D77;&#x4F86;&#x53EA;&#x6709;&#x6EFF;&#x6EFF;&#x7684;&#x611F;&#x52D5; (&#x5DF2;&#x7D93;&#x7121;&#x6CD5;&#x4E2D;&#x7ACB;&#x8A55;&#x8AD6;XD) &#x7E3D;&#x4E4B;&#x8ACB;&#x5728;&#x770B;2049&#x4E4B;&#x524D;&#x4E00;&#x5B9A;&#x8981;&#x770B;&#x904E;1982&#x6216;&#x81F3;&#x5C11;&#x77E5;&#x9053;&#x524D;&#x4F5C;&#x7684;&#x6545;&#x4E8B;&#xFF0C;&#x7136;&#x5F8C;&#x559C;&#x611B;cyberpunk&#x7684;&#x8A18;&#x5F97;&#x53BB;&#x96FB;&#x5F71;&#x9662;&#x770B;&#xFF0C;&#x96D6;&#x7136;&#x8AAA;&#x73FE;&#x5728;&#x53EA;&#x5269;&#x53F0;&#x5317;&#x51B7;&#x9580;&#x6642;&#x6BB5;&#x6709;&#x4E86;XD<br>&#x9019;&#x90E8;&#x5728;IMDB&#x62FF;&#x5230;8.4&#x9AD8;&#x5206;&#x7684;&#x96FB;&#x5F71;&#x5728;&#x7968;&#x623F;&#x4E0A;&#x5012;&#x662F;&#x6709;&#x9EDE;&#x60BD;&#x6158;&#xFF0C;&#x5BE6;&#x5728;&#x5F88;&#x53EF;&#x60DC;&#xFF0C;&#x79D1;&#x5E7B;&#x7247;&#x5728;&#x9019;&#x500B;&#x5E74;&#x4EE3;&#x5DF2;&#x7D93;&#x662F;&#x5FA9;&#x53E4;&#x7684;&#x5B58;&#x5728;&#x4E86;XDDD</p>]]></content:encoded></item><item><title><![CDATA[歡迎來到真實世界 - Unit Test for Core Data]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/container-terminal-closeup-PWHXT7A.jpg" class="kg-image" alt loading="lazy"></figure><h2 id="introduction">Introduction</h2><blockquote>Once an idea has taken hold of the brain it&apos;s almost impossible to eradicate. - Cobb <br>&#x4E00;&#x65E6;&#x67D0;&#x500B;&#x60F3;&#x6CD5;&#x638C;&#x63E1;&#x4E86;&#x4F60;&#x7684;&#x8166;&#x888B;&#xFF0C;&#x5B83;&#x5C07;&#x6703;&#x8B8A;&#x5F97;&#x96E3;&#x88AB;&#x93DF;&#x9664;&#x3002;-Cobb</blockquote><p>Inception(&#x5168;&#x9762;&#x555F;&#x52D5;)&#x4E00;</p>]]></description><link>https://huangshihting.works/blog/unit-test-for-core-data/</link><guid isPermaLink="false">61b970b956cf0e0001441753</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Thu, 05 Oct 2017 04:36:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/container-terminal-closeup-PWHXT7A.jpg" class="kg-image" alt loading="lazy"></figure><h2 id="introduction">Introduction</h2><blockquote>Once an idea has taken hold of the brain it&apos;s almost impossible to eradicate. - Cobb <br>&#x4E00;&#x65E6;&#x67D0;&#x500B;&#x60F3;&#x6CD5;&#x638C;&#x63E1;&#x4E86;&#x4F60;&#x7684;&#x8166;&#x888B;&#xFF0C;&#x5B83;&#x5C07;&#x6703;&#x8B8A;&#x5F97;&#x96E3;&#x88AB;&#x93DF;&#x9664;&#x3002;-Cobb</blockquote><p>Inception(&#x5168;&#x9762;&#x555F;&#x52D5;)&#x4E00;&#x76F4;&#x90FD;&#x662F;&#x500B;&#x4EBA;&#x6700;&#x611B;&#x7684;&#x96FB;&#x5F71;&#x4E4B;&#x4E00;&#xFF0C;&#x5287;&#x60C5;&#x71D2;&#x8166;&#x3001;&#x4E16;&#x754C;&#x89C0;&#x5B8C;&#x6574;&#x3001;&#x756B;&#x9762;&#x5922;&#x5E7B;&#xFF0C;&#x800C;&#x4E14;&#x53C8;&#x662F;&#x9019;&#x7A2E;&#x7A7F;&#x8D8A;&#x865B;&#x5BE6;&#x7684;&#x984C;&#x6750;&#xFF0C;&#x5E7E;&#x4E4E;&#x6240;&#x6709;&#x5143;&#x7D20;&#x90FD;&#x6DF1;&#x5F97;&#x6211;&#x5FC3;&#x3002;</p><p>&#x9032;&#x5165;&#x5922;&#x5883;&#x5C31;&#x96FB;&#x5F71;&#x4F86;&#x8AAA;&#x4E0D;&#x7B97;&#x592A;&#x65B0;&#x7684;&#x6897;&#xFF0C;Inception&#x8DDF;&#x5176;&#x5B83;&#x5922;&#x5883;&#x96FB;&#x5F71;&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#x662F;&#xFF0C;&#x9032;&#x5165;&#x5922;&#x5883;&#x6709;&#x500B;&#x66F4;&#x8907;&#x96DC;&#x7684;&#x539F;&#x56E0;&#xFF1A;&#x8981;&#x5FB9;&#x5E95;&#x6539;&#x8B8A;&#x67D0;&#x500B;&#x4EBA;&#x7684;&#x60F3;&#x6CD5;&#x3002;&#x8001;&#x6897;&#x96FB;&#x5F71;&#x5982;&#x679C;&#x9032;&#x5165;&#x67D0;&#x4EBA;&#x7684;&#x5922;&#x5883;&#xFF0C;&#x5F88;&#x6709;&#x53EF;&#x80FD;&#x5C31;&#x53EA;&#x662F;&#x8981;&#x5F15;&#x51FA;&#x4E00;&#x4E9B;&#x79D8;&#x5BC6;&#xFF0C;&#x50CF;&#x662F;&#x91D1;&#x5EAB;&#x5BC6;&#x78BC;&#x4E4B;&#x985E;&#x7684;(&#x8166;&#x6D77;&#x4E2D;&#x99AC;&#x4E0A;&#x6D6E;&#x73FE;&#xFF1A;&#x5DE6;&#x4E09;&#x3001;&#x53F3;&#x4E8C;&#x3001;&#x5DE6;&#x4E00;)&#xFF0C;&#x4F46;&#x662F;&#x5728;Inception&#x88E1;&#xFF0C;&#x9032;&#x5165;&#x5225;&#x4EBA;&#x7684;&#x5922;&#x5883;&#x662F;&#x70BA;&#x4E86;&#x8981;&#x6539;&#x8B8A;&#x67D0;&#x4EBA;&#x7684;&#x60F3;&#x6CD5;&#xFF0C;&#x8B93;&#x4ED6;&#x4E56;&#x4E56;&#x5730;&#x807D;&#x8A71;&#x505A;&#x4E00;&#x4E9B;&#x5F88;&#x8822;&#x7684;&#x4E8B;&#x3002;&#x9019;&#x9EDE;&#x5176;&#x5BE6;&#x975E;&#x5E38;&#x7684;&#x6709;&#x8DA3;&#xFF0C;&#x5728;&#x751F;&#x6D3B;&#x4E2D;&#xFF0C;&#x7684;&#x78BA;&#x6709;&#x4E9B;&#x60F3;&#x6CD5;&#x6839;&#x690D;&#x5728;&#x8166;&#x6D77;&#x4E2D;&#xFF0C;&#x4E45;&#x4E45;&#x63EE;&#x4E4B;&#x4E0D;&#x53BB;&#xFF0C;&#x5C31;&#x50CF;&#x88AB;&#x8AB0;&#x690D;&#x5165;&#x4E86;&#x601D;&#x60F3;&#x4E00;&#x6A23;&#xFF0C;&#x6BD4;&#x65B9;&#x8AAA;&#x6208;&#x5DF4;&#x5951;&#x592B;&#x982D;&#x9AEE;&#x6700;&#x591A;&#x3001;&#x6D77;&#x73CA;&#x6700;&#x4E0D;&#x611B;&#x6253;&#x4ED7;&#xFF0C;&#x5230;&#x9577;&#x5927;&#x5F8C;&#x6211;&#x9084;&#x662F;&#x4E00;&#x76F4;&#x6DF1;&#x4FE1;&#x4E0D;&#x79FB;&#x3002;</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/MV5BMTM0MjUzNjkwMl5BMl5BanBnXkFtZTcwNjY0OTk1Mw@.jpg" class="kg-image" alt loading="lazy"></figure><p>&#x6240;&#x4EE5;&#x9019;&#x8DDF;&#x6211;&#x5011;&#x4ECA;&#x5929;&#x7684;&#x4E3B;&#x984C;&#x6709;&#x751A;&#x9EBC;&#x95DC;&#x4FC2;&#x5462;&#xFF1F;&#x975E;&#x5E38;&#x6709;&#x95DC;&#x4FC2;&#xFF01;&#x56E0;&#x70BA;&#x5F9E;&#x4E0A;&#x7BC7;&#x6587;&#x7AE0;&#x958B;&#x59CB;&#xFF0C;&#x5167;&#x5BB9;&#x5C31;&#x4E00;&#x76F4;&#x90FD;&#x570D;&#x7E5E;&#x8457;&#x4E00;&#x500B;&#x4E3B;&#x984C;&#xFF1A;&#x6211;&#x6700;&#x559C;&#x6B61;&#x7684;&#x96FB;&#x5F71;...&#x963F;&#x4E0D;&#x662F;&#xFF0C;&#x662F;&#x5982;&#x4F55;&#x5728;iOS&#x4E0A;&#x505A;&#x4E7E;&#x6DE8;&#x7684;&#x3001;&#x8207;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#x4E92;&#x76F8;&#x7368;&#x7ACB;&#x7684;&#x55AE;&#x5143;&#x6E2C;&#x8A66;&#x3002;</p><p>&#x5728;&#x4E0A;&#x6B21;&#x4ECB;&#x7D39;&#x5B8C;Depedency Injection&#x4E4B;&#x5F8C;&#xFF0C;&#x60F3;&#x5FC5;&#x5927;&#x5BB6;&#x61C9;&#x8A72;&#x90FD;&#x5F88;&#x4E86;&#x89E3;&#x5982;&#x4F55;&#x5728;&#x81EA;&#x5DF1;&#x7684;&#x6A21;&#x7D44;&#x88E1;&#xFF0C;&#x628A;&#x73FE;&#x5BE6;&#x74B0;&#x5883;&#x62BD;&#x6389;&#xFF0C;&#x690D;&#x5165;&#x4E00;&#x500B;&#x5047;&#x7684;&#x74B0;&#x5883;&#x4E86;&#x3002;&#x5982;&#x679C;&#x9084;&#x4E0D;&#x662F;&#x5F88;&#x6E05;&#x695A;Depedency Inject&#x5728;&#x6E2C;&#x8A66;&#x4E0A;&#x7684;&#x61C9;&#x7528;&#xFF0C;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x62D9;&#x505A;&#xFF1A;<a href>&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; - Unit Test for Networking</a>(https://www.codementor.io/koromiko/unit-test-for-networking-ahdpdqr5k)&#x3002;</p><p>&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x5EF6;&#x7E8C;&#x4E0A;&#x6B21;&#x7684;&#x4E3B;&#x984C;&#xFF0C;&#x7E7C;&#x7E8C;&#x4F86;&#x63A2;&#x8A0E;&#x9700;&#x8981;&#x8DDF;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x4E92;&#x52D5;&#x7684;&#x5143;&#x4EF6;&#x7684;Unit Test&#x3002;&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#xFF0C;&#x6211;&#x5011;&#x4E3B;&#x8981;&#x7814;&#x7A76;&#x7684;&#x984C;&#x76EE;&#x662F;&#xFF1A;&#x600E;&#x6A23;&#x505A;Core Data&#x7684;Unit Test&#x3002;</p><p>Core Data&#x662F;&#x4E00;&#x500B;iOS&#x4E0A;&#x9762;&#x8CC7;&#x6599;&#x7D50;&#x69CB;&#x7684;&#x5C01;&#x88DD;&#xFF0C;&#x5B83;&#x628A;&#x8DDF;&#x8CC7;&#x6599;&#x5EAB;&#x76F8;&#x95DC;&#x7684;&#x908F;&#x8F2F;&#x90FD;&#x5C01;&#x88DD;&#x8D77;&#x4F86;&#xFF0C;&#x4E26;&#x4E14;&#x63D0;&#x4F9B;&#x5927;&#x91CF;&#x9AD8;&#x968E;&#x7684;&#x65B9;&#x6CD5;&#xFF0C;&#x8B93;&#x4F60;&#x76E1;&#x91CF;&#x907F;&#x514D;&#x5728;&#x5132;&#x5B58;&#x8CC7;&#x6599;&#x6642;&#xFF0C;&#x9084;&#x9700;&#x8981;&#x628A;&#x8CC7;&#x6599;&#x5EAB;&#x908F;&#x8F2F;&#x8DDF;&#x5546;&#x696D;&#x908F;&#x8F2F;&#x90FD;&#x5BEB;&#x5728;&#x4E00;&#x8D77;&#xFF0C;&#x8B93;&#x5B58;&#x53D6;&#x8CC7;&#x6599;&#x8B8A;&#x7684;&#x66F4;&#x76F4;&#x89C0;&#x3002;&#x5982;&#x679C;&#x8981;&#x9806;&#x5229;&#x5730;&#x95B1;&#x8B80;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#xFF0C;&#x9700;&#x8981;&#x7684;&#x80CC;&#x666F;&#x77E5;&#x8B58;&#x5C31;&#x662F;Core Data&#x7684;&#x57FA;&#x672C;&#x904B;&#x4F5C;&#x539F;&#x7406;&#x3001;Core Data&#x7684;concurrency&#x7B49;&#x3002;&#x5982;&#x679C;&#x4E0D;&#x592A;&#x4E86;&#x89E3;Core Data&#x7684;&#x904B;&#x4F5C;&#xFF0C;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x4E00;&#x4E9B;&#x6559;&#x5B78;&#x6587;&#x7AE0;&#xFF1A;</p><p><a href>A Complete Core Data Application &#xB7; objc.io</a>(https://www.objc.io/issues/4-core-data/full-core-data-application)</p><p><a href>An Introductory Core Data Tutorial - Cocoacasts</a>(https://cocoacasts.com/core-data-tutorial/)</p><h2 id="tldr">TL;DR</h2><p>&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x6703;&#x5305;&#x542B;&#x4EE5;&#x4E0B;&#x4E3B;&#x984C;&#xFF1A;</p><ul><li>&#x4F7F;&#x7528;NSPersistentContainer&#x5EFA;&#x7F6E;Core Data stack</li><li>&#x8A2D;&#x5B9A;In-memory Persistent Store</li><li>&#x57FA;&#x672C;&#x6E2C;&#x8A66;&#x6280;&#x5DE7;&#xFF1A;Fake&#x3001;Stub</li><li>&#x5728;XCTestCase&#x5167;&#x505A;&#x975E;&#x540C;&#x6B65;&#x7684;&#x6E2C;&#x8A66;</li></ul><h2 id="requirement">Requirement</h2><p>&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E2D;&#x4F7F;&#x7528;&#x7684;&#x958B;&#x767C;&#x74B0;&#x5883;&#x70BA;&#xFF1A;</p><ul><li>Swift 3</li><li>iOS 10</li><li>Xcode 8</li></ul><p>&#x56E0;&#x70BA;&#x6211;&#x5011;&#x6703;&#x4F7F;&#x7528;NSPersistentContainer&#x4F86;&#x8A2D;&#x5B9A;Core Data Stack&#xFF0C;&#x6240;&#x4EE5;iOS 10&#x662F;&#x5FC5;&#x8981;&#x7684;&#x3002;</p><h2 id="our-task">Our task</h2><p>&#x4ECA;&#x5929;&#x6211;&#x5011;&#x7684;&#x76EE;&#x6A19;&#x662F;&#x8981;&#x5BE6;&#x505A;&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;todo list&#x7CFB;&#x7D71;&#xFF0C;&#x53EF;&#x4EE5;&#x65B0;&#x589E;&#x3001;&#x8B80;&#x53D6;&#x3001;&#x4FEE;&#x6539;&#x3001;&#x522A;&#x9664;(CRUD, create, read, update, delete) todo list&#xFF0C;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x9019;&#x500B;&#x7CFB;&#x7D71;&#xFF0C;&#x53EF;&#x4EE5;&#x5728;&#x95DC;&#x6389;app&#x518D;&#x6253;&#x958B;&#x4E4B;&#x5F8C;&#xFF0C;&#x9084;&#x80FD;&#x7E7C;&#x7E8C;&#x8A18;&#x5F97;&#x4E0A;&#x6B21;&#x4FEE;&#x6539;&#x7684;&#x8CC7;&#x6599;&#x3002;&#x6C92;&#x932F;&#xFF0C;&#x9019;&#x500B;todo list app&#x6BD4;&#x8166;&#x888B;&#x9084;&#x5F37;&#xFF01;</p><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684;&#x4EFB;&#x52D9;&#x4F86;&#x4E86;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x4E00;&#x500B;&#x7CFB;&#x7D71;&#x53EF;&#x4EE5;&#xFF1A;</p><ol><li>&#x65B0;&#x589E;&#x4E00;&#x500B;Todo</li><li>&#x53D6;&#x51FA;&#x6240;&#x6709;Todo</li><li>&#x522A;&#x9664;&#x4E00;&#x500B;Todo</li><li>&#x628A;&#x66AB;&#x5B58;&#x7684;&#x8CC7;&#x6599;&#x63A8;&#x9032;persistent store</li></ol><p>&#x56E0;&#x70BA;&#x624B;&#x6A5F;&#x4E0A;&#x8CC7;&#x6E90;&#x6709;&#x9650;&#xFF0C;&#x6211;&#x5011;&#x4E0D;&#x5E0C;&#x671B;&#x6BCF;&#x6B21;&#x5B58;&#x53D6;&#x90FD;&#x52D5;&#x5230;&#x771F;&#x7684;&#x8CC7;&#x6599;&#x5EAB;&#xFF0C;&#x800C;&#x662F;&#x8981;&#x5728;&#x771F;&#x7684;&#x6709;&#x9700;&#x8981;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x624D;&#x628A;&#x8CC7;&#x6599;&#x5BEB;&#x5165;&#x8CC7;&#x6599;&#x5EAB;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5728;&#x8A2D;&#x8A08;&#x9019;&#x500B;&#x7CFB;&#x7D71;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x6703;&#x628A;&#x5132;&#x5B58;&#x8CC7;&#x6599;&#x9019;&#x4EF6;&#x4E8B;&#x62BD;&#x51FA;&#x4F86;&#xFF08;&#x7B2C;4&#x9EDE;)&#xFF0C;&#x8B93;&#x5B83;&#x53EF;&#x4EE5;&#x7D0D;&#x5165;&#x5546;&#x696D;&#x908F;&#x8F2F;&#x7684;&#x8003;&#x91CF;&#x4E4B;&#x4E2D;&#xFF0C;&#x672A;&#x4F86;&#x5C31;&#x53EF;&#x4EE5;&#x8996;&#x60C5;&#x6CC1;&#xFF0C;&#x770B;&#x662F;&#x8981;&#x8B93;&#x4F7F;&#x7528;&#x8005;&#x6309;&#x4E0B;&#x5132;&#x5B58;&#x5F8C;&#x624D;&#x771F;&#x7684;&#x5132;&#x5B58;&#xFF0C;&#x6216;&#x662F;&#x8CC7;&#x6599;&#x7D2F;&#x7A4D;&#x5230;&#x4E00;&#x5B9A;&#x7684;&#x91CF;&#x624D;&#x5132;&#x5B58;&#x3002;</p><p>Core Data&#x5728;iOS 10&#x4E4B;&#x5F8C;&#xFF0C;&#x5176;&#x5BE6;&#x5DF2;&#x7D93;&#x5927;&#x5E45;&#x7C21;&#x5316;&#x4E86;&#x8A2D;&#x5B9A;&#x96E3;&#x5EA6;&#xFF0C;&#x4EE5;&#x5F80;&#x9700;&#x8981;&#x5BEB;&#x4E00;&#x5927;&#x5806;boilerplate&#x7684;&#x72C0;&#x6CC1;&#x5DF2;&#x7D93;&#x4E0D;&#x5FA9;&#x898B;&#x4E86;&#xFF0C;&#x5728;&#x4F7F;&#x7528;&#x4E0A;&#x4E5F;&#x4E0D;&#x4E00;&#x5B9A;&#x9700;&#x8981;&#x5F88;&#x6DF1;&#x5165;&#x7684;&#x4E86;&#x89E3;&#xFF0C;&#x5C31;&#x80FD;&#x5920;&#x505A;&#x57FA;&#x672C;&#x7684;&#x64CD;&#x4F5C;&#x3002;&#x96D6;&#x7136;&#x8A71;&#x662F;&#x9019;&#x9EBC;&#x8AAA;&#x6C92;&#x932F;&#xFF0C;&#x4F46;&#x5982;&#x679C;&#x80FD;&#x5920;&#x4E86;&#x89E3;&#x6574;&#x500B;Core Data stack&#xFF0C;&#x9084;&#x6709;&#x5404;&#x500B;&#x5143;&#x4EF6;&#x4E92;&#x52D5;&#x7684;&#x65B9;&#x5F0F;&#xFF0C;&#x5C0D;&#x65BC;&#x8A2D;&#x8A08;&#x6A21;&#x7D44;&#x8DDF;&#x5BEB;&#x6E2C;&#x8A66;&#x9084;&#x662F;&#x6709;&#x4E00;&#x5B9A;&#x7684;&#x5E6B;&#x52A9;&#x7684;&#xFF0C;&#x6240;&#x4EE5;&#x4E0B;&#x9762;&#x9019;&#x500B;&#x5C0F;&#x7BC0;&#xFF0C;&#x6703;&#x7C21;&#x55AE;&#x63D0;&#x4E00;&#x4E0B;Core Data stack&#x88E1;&#x9762;&#x6709;&#x90A3;&#x4E9B;&#x5143;&#x4EF6;&#xFF0C;&#x9084;&#x6709;&#x9019;&#x4E9B;&#x5143;&#x4EF6;&#x662F;&#x600E;&#x6A23;&#x4E92;&#x52D5;&#x7684;&#x3002;</p><h2 id="core-data-stack">Core Data stack</h2><p>Core Data stack&#x662F;&#x6574;&#x500B;Core Data&#x7684;&#x57FA;&#x672C;&#x67B6;&#x69CB;&#xFF0C;&#x63CF;&#x8FF0;Core Data&#x5728;&#x904B;&#x4F5C;&#x6642;&#xFF0C;&#x6709;&#x90A3;&#x4E9B;&#x5143;&#x4EF6;&#xFF0C;&#x9019;&#x4E9B;&#x5143;&#x4EF6;&#x5404;&#x81EA;&#x6703;&#x6709;&#x90A3;&#x4E9B;&#x4EFB;&#x52D9;&#x7B49;&#x7B49;&#x3002;</p><p>&#x6839;&#x64DA;Apple&#x7684;<a href>&#x6587;&#x4EF6;</a>(https://developer.apple.com/library/content/documentation/DataManagement/Devpedia-CoreData/coreDataStack.html)&#xFF0C;Core Data Stack&#x5206;&#x6210;&#x4E0B;&#x9762;&#x5E7E;&#x500B;&#x90E8;&#x4EFD;&#xFF1A;</p><ul><li>Managed Object Context (NSManagedObjectContext)&#xFF0C;&#x63D0;&#x4F9B;&#x4E0D;&#x540C;&#x7684;&#x904B;&#x884C;&#x74B0;&#x5883;&#x7D66;managed objects&#x3002;</li><li>Persistent Store Coordinator (NSPersistentStoreCoordinator)&#xFF0C;&#x8CA0;&#x8CAC;&#x6574;&#x5408;&#x6BCF;&#x500B;Data Stores&#x3002;</li><li>Managed Object Model (NSManagedObjectModel)&#xFF0C;&#x8CA0;&#x8CAC;&#x63CF;&#x8FF0;&#x8CC7;&#x6599;&#x7684;&#x7D50;&#x69CB;&#x3001;&#x9577;&#x76F8;&#x7B49;&#x7B49;&#x3002;</li><li>Persistent Object Store&#xFF0C;&#x8CA0;&#x8CAC;&#x5E95;&#x5C64;&#x7684;&#x8CC7;&#x6599;&#x5EAB;&#x5BEB;&#x5165;&#x8DDF;&#x8B80;&#x53D6;&#x7B49;&#x3002;</li></ul><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/coredatastack.002.png" class="kg-image" alt loading="lazy"></figure><p><a href>Core Data stack</a>(https://developer.apple.com/library/content/documentation/DataManagement/Devpedia-CoreData/coreDataStack.html)</p><p>&#x4EE5;&#x5F80;&#x8981;&#x8A2D;&#x5B9A;&#x597D;&#x9019;&#x500B;stack&#xFF0C;&#x9700;&#x8981;&#x5BEB;&#x975E;&#x5E38;&#x591A;&#x7684;code&#x4F86;&#x500B;&#x5225;&#x8A2D;&#x5B9A;&#x3002;&#x800C;&#x5728;iOS 10&#x4E4B;&#x5F8C;&#xFF0C;&#x5B98;&#x65B9;&#x63D0;&#x4F9B;&#x4E86;&#x4E00;&#x500B;&#x65B0;&#x7684;&#x985E;&#x5225;&#xFF1A;NSPersistentContainer&#xFF0C;&#x628A;&#x4E0A;&#x9762;&#x6574;&#x500B;stack&#x90FD;&#x6253;&#x5305;&#x8D77;&#x4F86;&#xFF0C;&#x73FE;&#x5728;&#x53EA;&#x8981;&#x521D;&#x59CB;&#x5316;&#x4E00;&#x500B;NSPersistentContainer&#xFF0C;&#x4E26;&#x4E14;&#x4EE3;&#x5165;&#x4E00;&#x4E9B;&#x53C3;&#x6578;&#xFF0C;&#x63A5;&#x4E0B;&#x4F86;&#x6240;&#x6709;&#x64CD;&#x4F5C;&#x90FD;&#x53EF;&#x4EE5;&#x570D;&#x7E5E;&#x5728;&#x9019;&#x500B;container&#x4E0A;&#x9762;&#x5C31;&#x597D;&#xFF0C;&#x76F8;&#x7576;&#x65B9;&#x4FBF;&#x3002;</p><h2 id="%E8%A8%AD%E5%AE%9A%E7%92%B0%E5%A2%83">&#x8A2D;&#x5B9A;&#x74B0;&#x5883;</h2><p>&#x77E5;&#x9053;&#x4E86;Core Data stack&#x662F;&#x600E;&#x6A23;&#x7684;&#x6771;&#x897F;&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x8981;&#x4F86;&#x8A2D;&#x5B9A;&#x6211;&#x5011;&#x7684;Core Data&#x74B0;&#x5883;&#x3002;&#x5982;&#x679C;&#x4F60;&#x662F;&#x5728;&#x555F;&#x52D5;Project&#x6642;&#xFF0C;&#x5C31;&#x52FE;&#x9078;<strong>Use Core Data</strong>&#xFF0C;&#x90A3;Xcode&#x6703;&#x81EA;&#x52D5;&#x5E6B;&#x4F60;&#x5728;AppDelegate&#x88E1;&#x9762;&#x7522;&#x751F;&#x5FC5;&#x8981;&#x7684;code&#xFF0C;&#x4F46;&#x5982;&#x679C;&#x4F60;&#x662F;&#x5F8C;&#x4F86;&#x624D;&#x52A0;&#x7684;&#xFF0C;&#x8A18;&#x5F97;&#x8981;&#x5728;AppDelegate&#x88E1;&#x9762;&#x8A2D;&#x5B9A;&#x597D;container&#xFF1A;</p><pre><code class="language-swift">lazy var persistentContainer: NSPersistentContainer = {
		let container = NSPersistentContainer(name: &quot;PersistentTodoList&quot;)
		container.loadPersistentStores(completionHandler: { (storeDescription, error) in
			if let error = error as NSError? {
				fatalError(&quot;Unresolved error \(error), \(error.userInfo)&quot;)
			}
		})
		return container
	}()
</code></pre><p>&#x4E00;&#x822C;&#x4F86;&#x8AAA;&#x4E00;&#x500B;App&#x53EA;&#x6703;&#x6709;&#x4E00;&#x500B;persistent store&#xFF0C;&#x9019;&#x4E5F;&#x5C31;&#x662F;&#x70BA;&#x751A;&#x9EBC;&#x6211;&#x5011;&#x9700;&#x8981;&#x4E00;&#x500B;&#x5168;&#x57DF;&#x7684;container&#xFF0C;&#x76EE;&#x7684;&#x5C31;&#x662F;&#x70BA;&#x4E86;&#x65B9;&#x4FBF;&#x8B93;&#x5176;&#x5B83;class&#x53D6;&#x7528;&#x76F8;&#x540C;&#x7684;container&#xFF0C;&#x800C;&#x4E0D;&#x6703;&#x9020;&#x6210;&#x6DF7;&#x6DC6;&#x3002;</p><p>&#x4E00;&#x500B;&#x57FA;&#x672C;&#x7684;Todo list&#xFF0C;&#x81F3;&#x5C11;&#x8981;&#x80FD;&#x5920;&#x5BEB;&#x4E0A;&#x8981;&#x505A;&#x7684;&#x4E8B;&#x9805;&#xFF0C;&#x4E26;&#x4E14;&#x8981;&#x80FD;&#x6A19;&#x793A;&#x662F;&#x5426;&#x5B8C;&#x6210;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684;Data Model&#x9700;&#x8981;&#x63CF;&#x8FF0;&#x4E00;&#x500B;&#x5177;&#x6709;&#x4EE5;&#x4E0B;attributes&#x7684;entity&#xFF1A;</p><ul><li>name (string)</li><li>finished (bool)</li></ul><p>&#x6240;&#x4EE5;&#x8A2D;&#x5B9A;&#x4E0A;&#x6703;&#x9577;&#x9019;&#x6A23;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/Screen-Shot-2017-09-03-at-14.47.41.png" class="kg-image" alt loading="lazy"></figure><h2 id="%E8%A8%AD%E8%A8%88todostoragemanager">&#x8A2D;&#x8A08;TodoStorageManager</h2><p>&#x8A2D;&#x5B9A;&#x5B8C;Core Data stack&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x958B;&#x59CB;&#x4F86;&#x8A2D;&#x8A08;&#x6211;&#x5011;&#x4E3B;&#x8981;&#x62FF;&#x4F86;&#x64CD;&#x4F5C;Core Data&#x7684;&#x985E;&#x5225;&#xFF1A;TodoStorageManager&#xFF0C;&#x9019;&#x500B;manager&#x7684;&#x529F;&#x7528;&#xFF0C;&#x5C31;&#x662F;&#x8CA0;&#x8CAC;&#x6240;&#x6709;&#x8DDF;Core Data&#x7684;&#x4E92;&#x52D5;&#xFF0C;&#x7C21;&#x5316;&#x64CD;&#x4F5C;&#x7684;&#x7D30;&#x7D50;&#xFF0C;&#x8B93;&#x8CC7;&#x6599;&#x8655;&#x7406;&#x53EF;&#x4EE5;&#x76E1;&#x91CF;&#x7368;&#x7ACB;&#x65BC;&#x5176;&#x5B83;&#x908F;&#x8F2F;&#x4E4B;&#x5916;&#x3002;&#x6211;&#x5011;&#x76EE;&#x524D;&#x898F;&#x5283;&#x7684;&#x529F;&#x80FD;&#xFF0C;&#x5C31;&#x662F;&#x7C21;&#x55AE;&#x7684;CRUD&#x3002;</p><p>&#x56E0;&#x70BA;&#x9019;&#x500B;manager&#x4E3B;&#x8981;&#x64CD;&#x4F5C;&#x7684;&#x74B0;&#x5883;&#x5C31;&#x662F;Core Data&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684;dependency&#x81EA;&#x7136;&#x800C;&#x7136;&#x5C31;&#x6703;&#x662F;persistent container&#xFF0C;&#x521D;&#x59CB;&#x5316;&#x8A2D;&#x5B9A;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-swift">class ToDoStorgeManager {
	
	let persistentContainer: NSPersistentContainer!
	
	//MARK: Init with dependency
	init(container: NSPersistentContainer) {
		self.persistentContainer = container
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
	}
	
	convenience init() {
		//Use the default container for production environment
		guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
			fatalError(&quot;Can not get shared app delegate&quot;)
		}
		self.init(container: appDelegate.persistentContainer)
	}
}
</code></pre><p>&#x4E0A;&#x9762;&#x8A2D;&#x5B9A;&#x4E86;&#x4E00;&#x500B;convenience init&#xFF0C;&#x653E;&#x5165;&#x9810;&#x8A2D;&#x7684;dependency&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x5168;&#x57DF;&#x7684;container&#xFF0C;&#x9019;&#x6A23;&#x4E00;&#x4F86;&#x5728;production&#x74B0;&#x5883;&#x8981;&#x53D6;&#x7528;&#x9019;&#x500B;manager&#x6703;&#x6BD4;&#x8F03;&#x7C21;&#x55AE;&#xFF0C;&#x4E5F;&#x6BD4;&#x8F03;&#x5BB9;&#x6613;&#x5728;&#x770B;code&#x6642;&#x5019;&#x5C31;&#x77E5;&#x9053;&#x5B83;&#x7684;&#x529F;&#x7528;&#x3002;</p><p>&#x53E6;&#x5916;&#xFF0C;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x5728;&#x64CD;&#x4F5C;Core Data&#x6642;&#xFF0C;&#x5BEB;&#x5165;&#x5728;&#x80CC;&#x666F;thread&#x57F7;&#x884C;&#xFF0C;&#x9019;&#x6A23;&#x53EF;&#x4EE5;&#x907F;&#x514D;&#x6548;&#x80FD;&#x4E0A;&#x7684;&#x554F;&#x984C;&#x3002;&#x5728;NSPersistentContainer&#x88E1;&#xFF0C;&#x9810;&#x8A2D;&#x6703;&#x63D0;&#x4F9B;&#x5169;&#x7A2E;context&#xFF1A;viewContext&#x8DDF;backgroundContext&#x3002;viewContext&#x5C31;&#x662F;&#x904B;&#x884C;&#x5728;main thread&#x4E0A;&#x7684;context&#xFF0C;&#x800C;&#x5229;&#x7528;NSPersistentContainer.newBackgroundContext()&#x7522;&#x751F;&#x7684;context&#xFF0C;&#x5C31;&#x662F;backgroundContext&#xFF0C;&#x6703;&#x5728;&#x80CC;&#x666F;thread&#x57F7;&#x884C;&#x8CC7;&#x6599;&#x5EAB;&#x64CD;&#x4F5C;&#x3002;</p><p>&#x5728;&#x9019;&#x908A;&#x6211;&#x5011;&#x9810;&#x8A2D;&#x6211;&#x5011;&#x7684;&#x80CC;&#x666F;&#x4EFB;&#x52D9;(&#x6240;&#x6709;&#x5BEB;&#x5165;&#x7684;&#x4EFB;&#x52D9;)&#x90FD;&#x53EA;&#x9700;&#x8981;&#x5728;&#x540C;&#x4E00;&#x500B;&#x80CC;&#x666F;thread&#x57F7;&#x884C;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x6703;&#x76F4;&#x63A5;&#x5EFA;&#x7ACB;&#x4E00;&#x500B;backgroundContext&#x4F9B;&#x4F7F;&#x7528;&#xFF1A;</p><pre><code class="language-swift">lazy var backgroundContext: NSManagedObjectContext = {
		return self.persistentContainer.newBackgroundContext()
}()
</code></pre><p>&#x6700;&#x5F8C;&#xFF0C;&#x64CD;&#x4F5C;&#x7684;&#x90E8;&#x4EFD;&#x5BE6;&#x4F5C;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-swift">//MARK: CRUD
func insertTodoItem( name: String, finished: Bool ) -&gt; ToDoItem? {

	guard let toDoItem = NSEntityDescription.insertNewObject(forEntityName: &quot;ToDoItem&quot;, into: backgroundContext) as? ToDoItem else { return nil }
	toDoItem.name = name
	toDoItem.finished = finished
	
	return toDoItem
}

func remove( objectID: NSManagedObjectID ) {
	let obj = backgroundContext.object(with: objectID)
	backgroundContext.delete(obj)
}

func fetchAll() -&gt; [ToDoItem] {
	let request: NSFetchRequest&lt;ToDoItem&gt; = ToDoItem.fetchRequest()
	let results = try? persistentContainer.viewContext.fetch(request)
	return results ?? [ToDoItem]()
}

func save() {
	if backgroundContext.hasChanges {
		do {
			try backgroundContext.save()
		} catch {
			print(&quot;Save error \(error)&quot;)
		}
	}	
}
</code></pre><p>&#x5230;&#x9019;&#x88E1;&#xFF0C;&#x6700;&#x7C21;&#x55AE;&#x7684;manager&#x5C31;&#x5BEB;&#x5B8C;&#x4E86;&#xFF0C;&#x5B83;&#x73FE;&#x5728;&#x53EF;&#x4EE5;&#x65B0;&#x589E;&#x3001;&#x8B80;&#x53D6;&#x8DDF;&#x522A;&#x9664;todo item&#x4E86;&#x3002;</p><h2 id="unit-test-setup">Unit Test Setup</h2><p>&#x63A5;&#x8457;&#x6211;&#x5011;&#x8981;&#x958B;&#x59CB;&#x4F86;&#x64B0;&#x5BEB;&#x6E2C;&#x8A66;&#x7684;code&#xFF0C;&#x4F9D;&#x7167;&#x6211;&#x5011;&#x7684;&#x898F;&#x5283;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x4EE5;&#x4E0B;&#x5E7E;&#x500B;&#x6E2C;&#x8A66;case&#xFF1A;</p><ol><li>&#x8981;&#x80FD;&#x6210;&#x529F;&#x65B0;&#x589E;Todo&#x4E26;&#x56DE;&#x50B3;ToDoItem&#x7269;&#x4EF6;</li><li>&#x8981;&#x80FD;&#x53D6;&#x5F97;&#x76EE;&#x524D;&#x8CC7;&#x6599;&#x5EAB;&#x4E2D;&#x7684;todos</li><li>&#x8981;&#x80FD;&#x6210;&#x529F;&#x522A;&#x9664;&#x8CC7;&#x6599;&#x5EAB;&#x4E2D;&#x7684;todo</li><li>&#x547C;&#x53EB;save()&#xFF0C;&#x8981;&#x80FD;&#x5920;&#x5C0D;&#x8CC7;&#x6599;&#x5EAB;&#x57F7;&#x884C;Save</li></ol><p>&#x78BA;&#x5B9A;&#x4E86;&#x6211;&#x5011;&#x7684;&#x76EE;&#x6A19;&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x958B;&#x59CB;&#x4F86;&#x8A2D;&#x7F6E;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x4E86;&#xFF0C;&#x9996;&#x5148;&#x6211;&#x5011;&#x5148;&#x8A2D;&#x5B9A;&#x597D;&#x6211;&#x5011;&#x7684;SUT&#xFF1A;</p><pre><code class="language-swift">var sut: ToDoStorgeManager! 
override func setUp() {
	super.setUp()
	sut = ToDoStorgeManager(container: mockPersistantContainer)
}
</code></pre><p>&#x5176;&#x4E2D;mockPersistentContainer&#x5C31;&#x662F;&#x4E00;&#x500B;mock container&#xFF0C;&#x6211;&#x5011;&#x60F3;&#x8981;&#x62FF;&#x5B83;&#x4F86;&#x53D6;&#x4EE3;&#x771F;&#x7684;container&#x3002;&#x5C0D;Core Data&#x4F86;&#x8AAA;&#xFF0C;persistent store&#x61C9;&#x8A72;&#x8981;&#x662F;sqlite&#x6216;&#x662F;binary store&#xFF0C;&#x9019;&#x6A23;&#x624D;&#x80FD;&#x628A;&#x8CC7;&#x6599;&#x6C38;&#x4E45;&#x5132;&#x5B58;&#x4E0B;&#x4F86;&#x3002;&#x554F;&#x984C;&#x662F;&#xFF0C;&#x9019;&#x5169;&#x6A23;&#x90FD;&#x662F;&#x6211;&#x5011;&#x76EE;&#x524D;&#x7121;&#x6CD5;mock&#x7684;&#x6771;&#x897F;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x9700;&#x8981;&#x5229;&#x7528;&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#x6E2C;&#x8A66;&#x6280;&#x5DE7;&#xFF0C;&#x4F86;&#x9054;&#x5230;&#x66FF;&#x63DB;&#x74B0;&#x5883;&#x7684;&#x76EE;&#x7684;&#x3002;</p><h2 id="fakein-memory-data-store">Fake - In-memory data store</h2><p>Core Data&#x672C;&#x8EAB;&#x6709;&#x63D0;&#x4F9B;&#x4E00;&#x7A2E;&#x7279;&#x5225;&#x7684;store type&#xFF1A;NSInMemoryStoreType&#x3002;&#x9019;&#x500B;NSInMemoryStoreType&#xFF0C;&#x5C0D;Core Data&#x4F86;&#x8AAA;&#xFF0C;&#x4E5F;&#x662F;&#x4E00;&#x7A2E;persistent store&#xFF0C;&#x8DDF;sqlite&#x6216;&#x662F;binary&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#x5730;&#x65B9;&#x662F;&#xFF0C;&#x9019;&#x500B;store&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x53EA;&#x6703;&#x5132;&#x5B58;&#x5728;&#x8A18;&#x61B6;&#x9AD4;&#x88E1;&#x9762;&#x3002;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#x53EA;&#x8981;App&#x4E00;&#x95DC;&#x6389;&#xFF0C;&#x6240;&#x6709;&#x7684;&#x8CC7;&#x6599;&#x5C31;&#x6703;&#x6D88;&#x5931;&#x3002;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x5229;&#x7528;&#x9019;&#x6A23;&#x7684;&#x7279;&#x6027;&#xFF0C;&#x4F86;&#x505A;&#x4E00;&#x500B;&#x5047;&#x7684;&#x3001;&#x53EA;&#x5B58;&#x5728;&#x8A18;&#x61B6;&#x9AD4;&#x88E1;&#x9762;&#x7684;store&#xFF0C;&#x8B93;unit test code&#x53EF;&#x4EE5;&#x81EA;&#x7531;&#x53D6;&#x7528;&#xFF0C;&#x53C8;&#x4E0D;&#x7528;&#x64D4;&#x5FC3;&#x8CC7;&#x6599;&#x8DDF;production&#x7684;&#x641E;&#x6DF7;&#x3002;&#x9019;&#x7A2E;&#x65B9;&#x6CD5;&#xFF0C;&#x901A;&#x5E38;&#x5728;&#x6E2C;&#x8A66;&#x6280;&#x5DE7;&#x88E1;&#x9762;&#x88AB;&#x7A31;&#x505A;&#x70BA;&#xFF1A;Fake&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x900F;&#x904E;&#x88FD;&#x4F5C;&#x4E00;&#x500B;&#x975E;&#x5E38;&#x63A5;&#x8FD1;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#x7684;&#x7269;&#x4EF6;&#xFF0C;&#x4F86;&#x8B93;&#x6E2C;&#x8A66;&#x80FD;&#x5920;&#x76E1;&#x53EF;&#x80FD;&#x8207;&#x771F;&#x5BE6;&#x9694;&#x96E2;&#xFF0C;&#x4F46;&#x53C8;&#x80FD;&#x50CF;&#x662F;&#x5728;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#x4E2D;&#x57F7;&#x884C;&#x4E00;&#x6A23;&#x3002;</p><blockquote><em>Objects actually have working implementations, but usually take some shortcut which makes them not suitable for production</em> - Martin Fowler</blockquote><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5C31;&#x5229;&#x7528;&#x9019;&#x500B;&#x5047;&#x8CC7;&#x6599;&#x5EAB;&#xFF0C;&#x4F86;&#x88FD;&#x4F5C;&#x6211;&#x5011;&#x7684;mock container&#x5427;&#xFF01;</p><pre><code>lazy var mockPersistantContainer: NSPersistentContainer = {

	let container = NSPersistentContainer(name: &quot;PersistentTodoList&quot;, managedObjectModel: self.managedObjectModel)
	let description = NSPersistentStoreDescription()
	description.type = NSInMemoryStoreType
	description.shouldAddStoreAsynchronously = false // Make it simpler in test env
	
	container.persistentStoreDescriptions = [description]
	container.loadPersistentStores { (description, error) in
		// Check if the data store is in memory
		precondition( description.type == NSInMemoryStoreType )

		// Check if creating container wrong
		if let error = error {
			fatalError(&quot;Create an in-mem coordinator failed \(error)&quot;)
		}
	}
	return container
}()
</code></pre><p>&#x8B93;&#x6211;&#x5011;&#x4F86;&#x4ED4;&#x7D30;&#x5206;&#x6790;&#x4E00;&#x4E0B;&#x88E1;&#x9762;&#x7684;&#x5167;&#x5BB9;&#xFF1A;</p><pre><code class="language-swift">let container = NSPersistentContainer(name: &quot;PersistentTodoList&quot;, managedObjectModel: self.managedObjectModel)
</code></pre><p>&#x9019;&#x884C;&#x6703;&#x521D;&#x59CB;&#x5316;&#x4E00;&#x500B;container&#xFF0C;&#x4E26;&#x4E14;&#x6307;&#x5B9A;managedModel&#x3002;&#x56E0;&#x70BA;&#x5728;&#x6211;&#x5011;&#x7684;test target&#x88E1;&#x9762;&#xFF0C;namespace&#x8DDF;production target&#x662F;&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#xFF0C;&#x5982;&#x679C;&#x6211;&#x5011;&#x4E0D;&#x6307;&#x5B9A;managedModel&#xFF0C;NSPersistentContainer&#x6703;&#x7121;&#x6CD5;&#x6293;&#x5230;&#x6211;&#x5011;&#x7684;&#x7684;managedModel&#xFF0C;&#x4E5F;&#x5C31;&#x662F;.xcdatamodeld&#x6A94;&#x6848;&#x3002;&#x6240;&#x4EE5;&#x9019;&#x908A;&#x6211;&#x5011;&#x5FC5;&#x9808;&#x8981;&#x624B;&#x52D5;&#x8B93;test code&#x5148;&#x6293;&#x5230;&#x90A3;&#x500B;.xcdatamodeld&#xFF0C;&#x7136;&#x5F8C;&#x9935;&#x9032;container&#x88E1;&#x9762;&#xFF0C;&#x5982;&#x6B64;&#x4E00;&#x4F86;&#x624D;&#x80FD;&#x5920;&#x5171;&#x4EAB;&#x540C;&#x6A23;&#x7684;database schema&#x3002;&#x9019;&#x500B;managedObjectModel&#x8A2D;&#x5B9A;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-swift">lazy var managedObjectModel: NSManagedObjectModel = {
	let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: type(of: self))] )!
	return managedObjectModel
}()
</code></pre><p>&#x76F4;&#x63A5;&#x900F;&#x904E;bundle&#x628A;managedObjectModel&#x6293;&#x9032;test target&#x4F7F;&#x7528;&#xFF0C;&#x800C;&#x4E0D;&#x4EF0;&#x8CF4;NSPersistentContainer&#x7684;&#x81EA;&#x52D5;&#x5224;&#x65B7;&#x6A5F;&#x5236;&#x3002;&#x53E6;&#x5916;&#xFF0C;&#x56E0;&#x70BA;&#x9810;&#x8A2D;.xcdatamodeld&#x662F;&#x4E0D;&#x6703;compile&#x9032;test target&#x7684;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4E5F;&#x9700;&#x8981;&#x628A;.xcdatamodeld&#x9019;&#x500B;&#x6A94;&#x6848;&#x52A0;&#x5230;test target&#x88E1;&#x9762;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2021/12/Screen-Shot-2017-09-02-at-21.42.12.png" class="kg-image" alt loading="lazy"></figure><p>&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x5C31;&#x662F;&#x9019;&#x500B;Fake&#x7684;&#x95DC;&#x9375;&#xFF1A;</p><pre><code class="language-swift">let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
</code></pre><p>container&#x7684;&#x5C6C;&#x6027;&#xFF0C;&#x53EF;&#x4EE5;&#x900F;&#x904E;NSPersistentStoreDescription&#x4F86;&#x6307;&#x5B9A;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5C31;&#x628A;&#x6211;&#x5011;&#x7684;store&#x8A2D;&#x5B9A;&#x6210;&#x662F;InMemoryType&#xFF0C;&#x73FE;&#x5728;&#xFF0C;&#x5C0D;&#x9019;&#x500B;container&#x7684;&#x64CD;&#x4F5C;&#xFF0C;&#x5C31;&#x50CF;&#x5728;&#x5922;&#x5883;&#x88E1;&#x4E00;&#x6A23;&#xFF0C;&#x4E0D;&#x7BA1;&#x4F60;&#x600E;&#x6A23;&#x505A;&#xFF0C;&#x90FD;&#x4E0D;&#x6703;&#x5F71;&#x97FF;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x4E86;&#xFF01;</p><h2 id="stubcanned-responses">Stub - Canned responses</h2><blockquote><em>Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what&apos;s programmed in for the test.</em> - Martin Fowler</blockquote><p>&#x6839;&#x64DA;Martin Fowler&#x7684;&#x5B9A;&#x7FA9;&#xFF0C;stub&#x5C31;&#x662F;&#x4E00;&#x4E9B;&#x5DF2;&#x7D93;&#x5BEB;&#x6B7B;&#x7684;&#x7F50;&#x982D;&#x56DE;&#x50B3;&#x503C;&#xFF0C;&#x5E38;&#x898B;&#x7684;&#x4F7F;&#x7528;&#x60C5;&#x5883;&#x662F;&#xFF1A;&#x6211;&#x8981;&#x6E2C;&#x8A66;&#x6211;&#x7684;parser&#x6B63;&#x4E0D;&#x6B63;&#x78BA;&#xFF0C;&#x6211;&#x5C31;&#x5148;&#x6E96;&#x5099;&#x597D;&#x4E00;&#x4EFD;raw data&#xFF0C;&#x8B93;&#x5B83;&#x901A;&#x904E;parser&#xFF0C;&#x6700;&#x5F8C;&#x6BD4;&#x5C0D;&#x51FA;&#x4F86;&#x7684;&#x7D50;&#x679C;&#x8DDF;&#x6211;&#x6E96;&#x5099;&#x7684;&#x8CC7;&#x6599;&#x6709;&#x6C92;&#x6709;&#x5C0D;&#x61C9;&#x3002;&#x5728;&#x6211;&#x5011;&#x7684;&#x9019;&#x500B;case&#xFF0C;&#x6211;&#x5011;&#x5229;&#x7528;stub&#xFF0C;&#x4F86;&#x6E2C;&#x8A66;&#x6211;&#x5011;&#x5C0D;&#x8CC7;&#x6599;&#x5EAB;&#x7684;&#x64CD;&#x4F5C;&#xFF0C;&#x662F;&#x4E0D;&#x662F;&#x6709;&#x771F;&#x7684;&#x5728;&#x9032;&#x884C;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#xFF0C;&#x6211;&#x5011;&#x6E96;&#x5099;&#x4E00;&#x4E9B;stubs&#xFF0C;&#x628A;&#x5B83;&#x5011;&#x585E;&#x9032;&#x525B;&#x525B;&#x7684;&#x5047;&#x8CC7;&#x6599;&#x5EAB;&#x4E2D;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x5DF2;&#x7D93;&#x9810;&#x5148;&#x77E5;&#x9053;&#x8CC7;&#x6599;&#x5EAB;&#x4E2D;&#x6709;&#x90A3;&#x4E9B;&#x8CC7;&#x6599;&#x4E86;&#xFF0C;&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x6E2C;&#x8A66;&#x57F7;&#x884C;&#x65B0;&#x589E;&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6703;&#x591A;&#x4E00;&#x7B46;&#x8CC7;&#x6599;&#xFF0C;&#x522A;&#x9664;&#x662F;&#x4E0D;&#x662F;&#x6703;&#x5C11;&#x4E00;&#x7B46;&#x8CC7;&#x6599;&#x7B49;&#x7B49;&#x3002;&#x4EE5;&#x4E0B;&#x662F;&#x6211;&#x5011;&#x7522;&#x751F;Stub&#x7684;&#x65B9;&#x6CD5;&#xFF1A;</p><pre><code class="language-swift">func initStubs() {
	
	func insertTodoItem( name: String, finished: Bool ) -&gt; ToDoItem? {
		
		let obj = NSEntityDescription.insertNewObject(forEntityName: &quot;ToDoItem&quot;, into: mockPersistantContainer.viewContext)
		
		obj.setValue(&quot;1&quot;, forKey: &quot;name&quot;)
		obj.setValue(false, forKey: &quot;finished&quot;)

		return obj as? ToDoItem
	}
	
	_ = insertTodoItem(name: &quot;1&quot;, finished: false)
	_ = insertTodoItem(name: &quot;2&quot;, finished: false)
	_ = insertTodoItem(name: &quot;3&quot;, finished: false)
	_ = insertTodoItem(name: &quot;4&quot;, finished: false)
	_ = insertTodoItem(name: &quot;5&quot;, finished: false)
	
	do {
		try mockPersistantContainer.viewContext.save()
	}  catch {
		print(&quot;create fakes error \(error)&quot;)
	}
	
}
</code></pre><p>&#x5728;&#x9019;&#x908A;&#xFF0C;&#x6211;&#x5011;&#x76F4;&#x63A5;&#x64CD;&#x4F5C;&#x8CC7;&#x6599;&#x5EAB;&#xFF0C;&#x653E;&#x9032;&#x4E94;&#x7B46;&#x8CC7;&#x6599;&#xFF0C;&#x7576;&#x4F5C;&#x6211;&#x5011;&#x7684;stubs&#x3002;&#x9700;&#x8981;&#x6CE8;&#x610F;&#x7684;&#x662F;&#xFF0C;&#x56E0;&#x70BA;namespace&#x4E0D;&#x540C;&#x7684;&#x95DC;&#x4FC2;&#xFF0C;&#x6211;&#x5011;&#x7684;insert&#x8DDF;edit&#x90FD;&#x4E0D;&#x80FD;&#x76F4;&#x63A5;&#x4F7F;&#x7528;ToDoItem&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x4E0D;&#x80FD;&#x7528;Xcode&#x81EA;&#x52D5;&#x7522;&#x751F;&#x7684;NSManagedObject subclass&#xFF0C;&#x800C;&#x662F;&#x8981;&#x7528;&#x6700;&#x539F;&#x59CB;&#x7684;&#x64CD;&#x4F5C;Core Data&#x7684;&#x65B9;&#x5F0F;&#xFF1A;&#x7528;NSManagedObject.setValue(<em>, forKey:)&#x4F86;&#x5B58;&#x53D6;&#x8CC7;&#x6599;&#xFF0C;&#x4E0D;&#x7136;&#x6703;&#x767C;&#x751F;&#x627E;&#x4E0D;&#x5230;entity&#x7684;&#x554F;&#x984C;&#x3002;&#x5049;&#x54C9;Xcode&#xFF01;&#x53E6;&#x5916;&#xFF0C;&#x9019;&#x908A;&#x4F7F;&#x7528;viewContext&#x4F86;&#x505A;&#x5BEB;&#x5165;&#xFF0C;&#x662F;&#x56E0;&#x70BA;&#x6211;&#x5E0C;&#x671B;&#x8B93;&#x9019;&#x500B;&#x5BEB;&#x5165;&#x662F;&#x540C;&#x6B65;&#x7684;&#xFF0C;&#x8981;&#x78BA;&#x4FDD;&#x9019;&#x4E9B;Stub&#x5728;test case&#x57F7;&#x884C;&#x524D;&#xFF0C;&#x5C31;&#x5DF2;&#x7D93;&#x7522;&#x751F;&#x597D;&#x4E26;&#x4E14;&#x653E;&#x5165;&#x8CC7;&#x6599;&#x5EAB;&#x3002;</em></p><p>&#x518D;&#x4F86;&#xFF0C;&#x5C0D;Unit Test&#x4F86;&#x8AAA;&#xFF0C;&#x6BCF;&#x8DD1;&#x4E00;&#x500B;test case&#xFF0C;&#x90FD;&#x8981;&#x50CF;&#x662F;&#x5C55;&#x958B;&#x65B0;&#x7684;&#x4EBA;&#x751F;&#x4E00;&#x6A23;&#xFF0C;&#x8981;&#x662F;&#x5168;&#x65B0;&#x7684;&#x7A7A;&#x767D;&#x7684;&#x3002;&#x5C0F;&#x9B6F;&#x7684;&#x4EBA;&#x751F;&#x662F;&#x9ED1;&#x767D;&#x7684;&#xFF0C;&#x800C;&#x6E2C;&#x8A66;&#x66F4;&#x6158;&#xFF0C;&#x53EA;&#x80FD;&#x662F;&#x7A7A;&#x767D;&#x7684;&#x3002;&#x5343;&#x842C;&#x4E0D;&#x80FD;&#x51FA;&#x73FE;&#x9019;&#x500B;test case&#x88AB;&#x4E0A;&#x4E00;&#x500B;test case&#x5F71;&#x97FF;&#x7684;&#x72C0;&#x6CC1;&#xFF0C;&#x9019;&#x9EDE;&#x975E;&#x5E38;&#x91CD;&#x8981;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x8981;&#x6709;&#x4E00;&#x500B;&#x80FD;&#x5920;&#x628A;&#x8CC7;&#x6599;&#x5168;&#x90E8;&#x6E05;&#x6389;&#x7684;&#x65B9;&#x6CD5;&#xFF1A;</p><pre><code class="language-swift">func flushData() {
	let fetchRequest:NSFetchRequest&lt;NSFetchRequestResult&gt; = NSFetchRequest&lt;NSFetchRequestResult&gt;(entityName: &quot;ToDoItem&quot;)
	let objs = try! mockPersistantContainer.viewContext.fetch(fetchRequest)
	for case let obj as NSManagedObject in objs {
		mockPersistantContainer.viewContext.delete(obj)
	}
	try! mockPersistantContainer.viewContext.save()
}
</code></pre><p>&#x9019;&#x908A;&#x4E00;&#x6A23;&#xFF0C;&#x4E0D;&#x80FD;&#x5C0D;ToDoItem&#x9019;&#x500B;&#x81EA;&#x52D5;&#x751F;&#x6210;&#x7684;class&#x505A;&#x4EFB;&#x4F55;&#x64CD;&#x4F5C;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4F7F;&#x7528;&#x6700;&#x539F;&#x59CB;&#x7684;&#x65B9;&#x8A66;&#x64CD;&#x4F5C;Core Data&#xFF0C;&#x518D;&#x8DDF;&#x6211;&#x5FF5;&#x4E00;&#x6B21;&#xFF0C;&#x5049;&#x54C9;Xcode~</p><p>&#x628A;&#x9019;&#x5169;&#x500B;&#x65B9;&#x6CD5;&#x52A0;&#x5230;setUp()&#x8DDF;tearDown()&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x78BA;&#x4FDD;&#x6BCF;&#x500B;test case&#x90FD;&#x662F;&#x5D84;&#x65B0;&#x7684;&#x958B;&#x59CB;&#x4E86;&#xFF01;</p><pre><code class="language-swift">override func setUp() {
	super.setUp()
	initStubs() // Create stubs
	sut = ToDoStorgeManager(container: mockPersistantContainer)
}

override func tearDown() {
	flushData() // Clear all stubs
	super.tearDown()
}
</code></pre><h2 id="test-cases">Test Cases</h2><p>&#x7D93;&#x904E;&#x4E86;&#x9577;&#x6642;&#x9593;&#x7684;&#x92EA;&#x9673;&#xFF0C;&#x7D42;&#x65BC;&#x8981;&#x9032;&#x5165;&#x6211;&#x5011;&#x7684;&#x4E3B;&#x83DC;&#x4E86;&#xFF0C;&#x4E3B;&#x83DC;&#x76F8;&#x5C0D;&#x5F88;&#x7C21;&#x55AE;&#xFF0C;&#x53EA;&#x8981;&#x8DDF;&#x96A8; Given, When, Assert&#x9019;&#x6A23;&#x7684;&#x6A21;&#x5F0F;&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x9806;&#x5229;&#x5BEB;&#x51FA;&#x6BCF;&#x500B;test case&#xFF0C;&#x4EE5;&#x4E0B;&#x6211;&#x5011;&#x5148;&#x628A;&#x65B0;&#x589E;&#x3001;&#x8B80;&#x53D6;&#x3001;&#x8DDF;&#x79FB;&#x9664;&#x7684;test case&#x5BEB;&#x5B8C;&#xFF1A;</p><pre><code class="language-swift">func test_create_todo() {
	
	//Given the name &amp; status
	let name = &quot;Todo1&quot;
	let finished = false
	
	//When add todo
	let todo = sut.insertTodoItem(name: name, finished: finished)
	
	//Assert: return todo item
	XCTAssertNotNil( todo )

}

func test_fetch_all_todo() {
	
	//Given a storage with two todo
	
	//When fetch
	let results = sut.fetchAll()
	
	//Assert return two todo items
	XCTAssertEqual(results.count, 5)
}

func test_remove_todo() {
	
	//Given a item in persistent store
	let items = sut.fetchAll()
	let item = items[0]
	
	let numberOfItems = items.count
	
	//When remove a item
	sut.remove(objectID: item.objectID)
	sut.save()
	
	//Assert number of item - 1
	XCTAssertEqual(numberOfItemsInPersistentStore(), numberOfItems-1)
	
}

//Convenient method for getting the number of data in store now
func numberOfItemsInPersistentStore() -&gt; Int {
	let request: NSFetchRequest&lt;NSFetchRequestResult&gt; = NSFetchRequest(entityName: &quot;ToDoItem&quot;)
	let results = try! mockPersistantContainer.viewContext.fetch(request)
	return results.count
}
</code></pre><p>&#x5728;test<em>fetch</em>all<em>todo()&#x4E2D;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x5DF2;&#x7D93;&#x77E5;&#x9053;stub&#x7E3D;&#x5171;&#x7684;&#x6578;&#x91CF;&#x662F;5&#x500B;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x76F4;&#x63A5;&#x4F7F;&#x7528;5&#x4F86;&#x7576;&#x505A;&#x6211;&#x5011;&#x7684;assertion&#x3002;</em></p><p>&#x53E6;&#x5916;&#xFF0C;&#x5728;test<em>remove</em>todo()&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x505A;&#x4E86;&#x4E00;&#x4E9B;&#x59A5;&#x5354;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x7684;&#x5BEB;&#x5165;&#x662F;&#x5BEB;&#x5230;backgroundContext&#xFF0C;&#x4F46;&#x6211;&#x5011;&#x7684;numberOfItemsInPersistentStore()&#x537B;&#x662F;&#x8B80;viewContext&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x9700;&#x8981;&#x5148;&#x57F7;&#x884C;save()&#x9019;&#x500B;side effect&#xFF0C;&#x624D;&#x80FD;assert&#x6B63;&#x78BA;&#x7684;&#x6578;&#x91CF;&#x3002;&#x672A;&#x4F86;&#x53EF;&#x4EE5;&#x900F;&#x904E;mock&#x4E00;&#x500B;&#x5B8C;&#x5168;&#x5BA2;&#x5236;&#x5316;&#x7684;NSPersistentContainer&#xFF0C;&#x7522;&#x751F;&#x6307;&#x5B9A;&#x7684;backgroundContext&#x4F86;&#x89E3;&#x6C7A;&#x9019;&#x500B;&#x554F;&#x984C;&#xFF0C;&#x4E0D;&#x904E;&#x9019;&#x908A;&#x8B93;&#x6211;&#x5011;&#x5148;&#x5C08;&#x6CE8;&#x5728;fake&#x8DDF;stub&#x5C31;&#x597D;&#x3002;</p><p>&#x4E0A;&#x9762;&#x9019;&#x4E9B;&#x6771;&#x897F;&#x7684;&#x64CD;&#x4F5C;&#xFF0C;&#x5168;&#x90E8;&#x90FD;&#x662F;&#x5728;&#x8A18;&#x61B6;&#x9AD4;&#x4E2D;&#x64CD;&#x4F5C;&#xFF0C;&#x4F46;&#x5C0D;&#x6211;&#x5011;&#x7684;SUT&#x4F86;&#x8AAA;&#xFF0C;&#x56E0;&#x70BA;&#x63A5;&#x53E3;&#x4E00;&#x6A21;&#x4E00;&#x6A23;&#xFF0C;&#x90FD;&#x662F;NSPersistentContainer&#xFF0C;&#x6240;&#x4EE5;&#x4E92;&#x52D5;&#x7684;&#x65B9;&#x5F0F;&#x8DDF;production&#x662F;&#x4E00;&#x6A21;&#x4E00;&#x6A23;&#x7684;&#x3002;</p><h2 id="expectations-to-notification">Expectations to Notification</h2><p>&#x5230;&#x6B64;&#xFF0C;&#x6211;&#x5011;&#x9084;&#x6709;&#x4E00;&#x500B;case&#x9084;&#x6C92;&#x6709;&#x5BEB;&#x5230;&#xFF1A;</p><ol><li>&#x547C;&#x53EB;save()&#xFF0C;&#x8981;&#x80FD;&#x5920;&#x5C0D;&#x8CC7;&#x6599;&#x5EAB;&#x57F7;&#x884C;Save</li></ol><p>&#x9019;&#x500B;case&#x7684;&#x76EE;&#x7684;&#xFF0C;&#x662F;&#x8981;&#x78BA;&#x4FDD;&#x6211;&#x5011;&#x7684;ToDoStorageManager.save()&#x771F;&#x7684;&#x6709;&#x547C;&#x53EB;&#x5230;NSManagedObjectContext.save()&#xFF0C;&#x8B93;&#x8CC7;&#x6599;&#x771F;&#x7684;&#x6709;&#x5BEB;&#x5165;persistent store&#x3002;&#x4F9D;&#x7167;&#x6211;&#x5011;&#x5E73;&#x5E38;mock&#x7684;&#x4F5C;&#x6CD5;&#xFF0C;&#x6211;&#x5011;&#x61C9;&#x8A72;&#x8981;&#x5EFA;&#x7ACB;&#x4E00;&#x500B;NSManagedObjectContextProtocol&#xFF0C;&#x4E26;&#x4E14;&#x7522;&#x751F;&#x4E00;&#x500B;mock&#x4F86;conform&#x9019;&#x500B;protocol&#xFF0C;&#x7136;&#x5F8C;&#x5229;&#x7528;mock&#x4F86;&#x5F97;&#x77E5;NSManagedObjectContext.save()&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6709;&#x88AB;&#x547C;&#x53EB;(&#x518D;&#x6B21;&#x5BA3;&#x50B3;&#x4E00;&#x4E0B;&#xFF0C;&#x53EF;&#x4EE5;&#x53C3;&#x8003;<a href>&#x62D9;&#x4F5C;</a>(https://www.codementor.io/koromiko/unit-test-for-networking-ahdpdqr5k))&#x3002;&#x4F46;&#x554F;&#x984C;&#x4F86;&#x4E86;&#xFF0C;&#x6211;&#x5011;&#x7684;dependency&#x662F;NSPersistentContainer&#xFF0C;&#x6240;&#x6709;&#x7684;context&#x90FD;&#x88AB;&#x5C01;&#x88DD;&#x5728;&#x9019;&#x500B;container&#x4E4B;&#x4E2D;&#xFF0C;&#x4E26;&#x4E14;&#x76EE;&#x524D;&#x4E5F;&#x6C92;&#x6709;&#x65B9;&#x6CD5;&#x53EF;&#x4EE5;&#x5728;container&#x4E2D;&#x690D;&#x5165;&#x81EA;&#x5DF1;&#x7684;context&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5FC5;&#x9808;&#x8981;&#x627E;&#x5225;&#x7684;&#x65B9;&#x6CD5;&#x4F86;&#x505A;&#x9019;&#x500B;behavior test&#x3002;</p><p>&#x5982;&#x679C;&#x4E0D;&#x80FD;&#x900F;&#x904E;mock&#x4F86;&#x4E86;&#x89E3;save()&#x7684;&#x904B;&#x4F5C;&#xFF0C;&#x6211;&#x5011;&#x9084;&#x6709;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x59A5;&#x5354;&#x7684;&#x65B9;&#x6CD5;&#xFF0C;&#x5728;Core Data&#xFF0C;&#x53EA;&#x8981;context&#x6709;&#x8B8A;&#x52D5;&#xFF0C;&#x90FD;&#x6703;&#x767C;&#x51FA;&#x5C0D;&#x61C9;&#x7684;notification&#xFF0C;&#x6211;&#x5011;&#x53EA;&#x8981;&#x807D;&#x9019;&#x4E9B;notification&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x77E5;&#x9053;&#x6211;&#x5011;&#x7684;SUT&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6709;&#x5C0D;context&#x4E0B;save()&#x3002;&#x5728;&#x9019;&#x908A;&#x6211;&#x5011;&#x6703;&#x5C08;&#x6CE8;&#x5728;NSManagedObjectContextDidSave&#x9019;&#x500B;notification&#xFF0C;&#x9019;&#x500B;notification&#x6703;&#x5728;&#x8CC7;&#x6599;&#x88AB;&#x5B58;&#x56DE;persistent store&#x5F8C;&#x89F8;&#x767C;&#xFF0C;&#x53EA;&#x8981;&#x6709;&#x6536;&#x5230;&#x9019;&#x500B;notification&#xFF0C;&#x5C31;&#x8868;&#x793A;NSManagedContext.save()&#x662F;&#x6709;&#x88AB;&#x89F8;&#x767C;&#x7684;&#x3002;</p><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5728;setUp()&#x88E1;&#x9762;&#xFF0C;&#x8A3B;&#x518A;&#x9019;&#x500B;notification&#xFF1A;</p><pre><code class="language-swift">NotificationCenter.default.addObserver(self, selector: #selector(contextSaved(notification:)), name: NSNotification.Name.NSManagedObjectContextDidSave , object: nil)
</code></pre><p>&#x7136;&#x5F8C;&#x8A2D;&#x5B9A;&#x597D;&#x9019;&#x500B;notification&#x7684;handler&#xFF1A;</p><pre><code class="language-swift">func contextSaved( notification: Notification ) {
}
</code></pre><p>&#x63A5;&#x8457;&#x6211;&#x5011;&#x4F86;&#x770B;&#x4E00;&#x4E0B;&#x6211;&#x5011;save()&#x7684;&#x6E2C;&#x8A66;code&#xFF1A;</p><pre><code class="language-swift">func test_save() {
			
	//Give a todo item
	let name = &quot;Todo1&quot;
	let finished = false
	
	_ = expectationForSaveNotification()
	
	_ = sut.insertTodoItem(name: name, finished: finished)
	
	//When save
	sut.save()
	
	//Assert save is called via notification (wait)
	waitForExpectations(timeout: 1, handler: nil)

}
</code></pre><p>&#x5728;&#x9019;&#x4E00;&#x500B;case&#x4E4B;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x505A;&#x7684;&#x4E8B;&#x60C5;&#x662F;&#xFF1A;&#x65B0;&#x589E;&#x4E00;&#x500B;todo list&#xFF0C;&#x4E26;&#x4E14;&#x7B49;&#x5F85;<strong>NSManagedObjectContextDidSave</strong>(&#x4E5F;&#x5C31;&#x662F;expectationForSaveNotification)&#xFF0C;&#x5728;&#x4E00;&#x79D2;&#x5167;&#x6536;&#x5230;&#x9019;&#x500B;notification(&#x4E5F;&#x5C31;&#x662F;waitForExpectations)&#xFF0C;&#x9019;&#x500B;test case&#x624D;&#x7B97;&#x6709;&#x904E;&#x3002;&#x56E0;&#x70BA;notification&#x662F;&#x4E00;&#x500B;&#x975E;&#x540C;&#x6B65;&#x7684;&#x884C;&#x70BA;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x9700;&#x8981;&#x5229;&#x7528;XCTestExpectation&#xFF0C;&#x4F86;&#x505A;&#x975E;&#x540C;&#x6B65;&#x7684;&#x6E2C;&#x8A66;&#x3002;&#x8B93;&#x6211;&#x5011;&#x5148;&#x4F86;&#x770B;&#x4E00;&#x4E0B;expectationForSaveNotification()&#x7684;&#x5BE6;&#x4F5C;&#xFF1A;</p><pre><code class="language-swift">func expectationForSaveNotification() -&gt; XCTestExpectation {
	let expect = expectation(description: &quot;Context Saved&quot;)
	//After Do something async {
		expect.fulfill()
		// }
	return expect
}
</code></pre><p>expectation(description: &quot;Context Saved&#x201D;)&#x662F;&#x4E00;&#x500B;&#x5728;XCTestCase&#x5DF2;&#x7D93;&#x5B9A;&#x7FA9;&#x7684;method&#xFF0C;&#x76EE;&#x5730;&#x662F;&#x8981;&#x8A2D;&#x5B9A;&#x4E00;&#x500B;XCTestExpectation&#x7269;&#x4EF6;&#xFF0C;&#x4E26;&#x4E14;&#x5C07;&#x9019;&#x500B;expectation&#x52A0;&#x5165;&#x6E05;&#x55AE;&#xFF0C;&#x63A5;&#x8457;&#x53EA;&#x8981;&#x5728;&#x4F60;&#x7684;test case&#x88E1;&#x9762;&#x52A0;&#x4E0A;waitForExpectations(timeout: TimeInterval, handler: XCTest.XCWaitCompletionHandler? = nil)&#xFF0C;&#x90A3;&#x500B;test case&#x5C31;&#x6703;&#x4E00;&#x76F4;&#x7B49;&#x5F85;&#xFF0C;&#x7B49;&#x5230;expect.fulfill()&#x88AB;&#x89F8;&#x767C;&#x4E4B;&#x5F8C;&#xFF0C;&#x624D;&#x6703;&#x901A;&#x904E;&#x6E2C;&#x8A66;&#xFF0C;&#x5982;&#x679C;&#x5728;timeout&#x8A2D;&#x5B9A;&#x7684;&#x6642;&#x9593;&#x524D;&#xFF0C;&#x90FD;&#x6C92;&#x6536;&#x5230;&#x4EFB;&#x4F55;fulfill()&#x7684;&#x547C;&#x53EB;&#xFF0C;&#x9019;&#x500B;case&#x5C31;&#x6703;&#x88AB;&#x5224;&#x5B9A;&#x5931;&#x6557;&#x3002;</p><p>&#x63A5;&#x8457;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x7B49;&#x5F85;&#x7684;&#x662F;&#x4E00;&#x500B;notification&#xFF0C;&#x6240;&#x4EE5;&#x4E0A;&#x9762;&#x7684;After Do something async&#xFF0C;&#x6703;&#x9019;&#x6A23;&#x5BE6;&#x4F5C;&#xFF1A;</p><pre><code class="language-swift">//MARK: Convinient function for notification
var saveNotificationCompleteHandler: ((Notification)-&gt;())?

func waitForSavedNotification(completeHandler: @escaping ((Notification)-&gt;()) ) {
	saveNotificationCompleteHandler = completeHandler
}

func contextSaved( notification: Notification ) {
	saveNotificationCompleteHandler?(notification)
}
</code></pre><p>&#x5728;waitForSavedNotification&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x5148;&#x628A;complete handler&#x5B58;&#x4E0B;&#x4F86;&#xFF0C;&#x7B49;&#x5230;&#x5728;contextSaved(Notification:)&#x4E2D;&#x6536;&#x5230;notification&#x4E4B;&#x5F8C;&#xFF0C;&#x518D;&#x547C;&#x53EB;&#x9019;&#x500B;handler&#xFF0C;&#x9054;&#x5230;async&#x7684;&#x6548;&#x679C;&#xFF0C;&#x4E00;&#x65E6;&#x9019;&#x500B;complete handler&#x88AB;&#x547C;&#x53EB;&#x4E86;&#xFF0C;&#x5C31;&#x6703;&#x89F8;&#x767C;expect.fulfill()&#xFF0C;&#x800C;waitForExpectations&#x5C31;&#x6703;&#x88AB;&#x6EFF;&#x8DB3;&#xFF0C;&#x6E2C;&#x8A66;case&#x4E5F;&#x5C31;&#x9806;&#x5229;&#x901A;&#x904E;&#x4E86;&#xFF01;</p><h2 id="recap">Recap</h2><p>&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x5B78;&#x7FD2;&#x5230;&#x4E86;&#xFF1A;</p><ol><li>&#x600E;&#x6A23;&#x7528;NSPersistentContainer&#x8A2D;&#x5B9A;Core Data stack</li><li>&#x600E;&#x6A23;&#x8A2D;&#x5B9A;Fake&#x53CA;Stub for Core Data</li><li>&#x600E;&#x9EBC;&#x5728;XCTestCase&#x4E2D;&#x505A;&#x975E;&#x540C;&#x6B65;&#x7684;&#x6E2C;&#x8A66;</li></ol><p>&#x5B8C;&#x6574;&#x7684;&#x7A0B;&#x5F0F;&#x53EF;&#x4EE5;&#x5728;&#x6211;&#x7684;<a href>Github</a>(https://github.com/koromiko/Tutorial/tree/master/PersistentTodoList)&#x4E0A;&#x9762;&#x627E;&#x5230;&#x3002;</p><h2 id="summary">Summary</h2><p>&#x5C0D;Core Data&#x4F86;&#x8AAA;&#xFF0C;&#x5176;&#x5BE6;&#x6709;&#x8A31;&#x8A31;&#x591A;&#x591A;&#x7A2E;&#x6E2C;&#x8A66;&#x7684;&#x65B9;&#x6CD5;&#xFF0C;&#x6700;&#x5E38;&#x898B;&#x7684;&#x9084;&#x662F;mock NSManagedObjectContext&#xFF0C;&#x4E5F;&#x6709;&#x8A31;&#x591A;&#x6587;&#x7AE0;&#x5DF2;&#x7D93;&#x6709;&#x505A;&#x904E;&#x63A2;&#x8A0E;&#xFF1A;</p><p><a href>How to Create Mocks and Stubs in Swift - Andrew Bancroft</a>(https://www.andrewcbancroft.com/2014/07/15/how-to-create-mocks-and-stubs-in-swift/)</p><p><a href>Real-World Testing with XCTest &#xB7; objc.io</a>(https://www.objc.io/issues/15-testing/xctest/#core-data)</p><p>&#x4E0D;&#x904E;&#x70BA;&#x4E86;&#x8981;&#x7B26;&#x5408;iOS 10&#x4EE5;&#x5F8C;&#x65B0;&#x7684;Core Data stack&#x7528;&#x6CD5;&#xFF0C;&#x5C0F;&#x5F1F;&#x8A66;&#x8457;&#x505A;&#x4E86;&#x4E00;&#x4E9B;&#x7814;&#x7A76;&#xFF0C;&#x8A66;&#x8457;&#x5728;NSPersistentContainer&#x7684;&#x72C0;&#x6CC1;&#x4E0B;&#x505A;&#x6E2C;&#x8A66;&#xFF0C;&#x96D6;&#x7136;&#x5F88;&#x591A;&#x5730;&#x65B9;&#x9700;&#x8981;&#x7A0D;&#x5FAE;&#x7E5E;&#x8DEF;&#x4E00;&#x4E0B;&#xFF0C;&#x4F46;&#x6574;&#x9AD4;&#x4F86;&#x8AAA;&#x8A72;&#x6E2C;&#x8A66;&#x7684;&#x5730;&#x65B9;&#x90FD;&#x9084;&#x662F;&#x53EF;&#x4EE5;&#x5B8C;&#x6574;&#x5730;&#x6E2C;&#x8A66;&#xFF0C;&#x4E26;&#x4E14;&#x4E5F;&#x76F8;&#x7576;&#x7368;&#x7ACB;&#x65BC;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#x4E4B;&#x5916;&#xFF0C;&#x5E0C;&#x671B;&#x9019;&#x4E9B;&#x65B9;&#x6CD5;&#x80FD;&#x5920;&#x5E36;&#x4F86;&#x4E00;&#x9EDE;&#x5E6B;&#x52A9;&#x3002;</p><p>&#x6700;&#x5F8C;&#xFF0C;&#x5728;&#x5BEB;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x7684;&#x540C;&#x6642;&#xFF0C;&#x5DF2;&#x7D93;&#x8D8A;&#x4F86;&#x8D8A;&#x591A;&#x4EBA;&#x6295;&#x5165;Realm&#x7684;&#x61F7;&#x62B1;&#x4E86;&#xFF0C;&#x627E;&#x4E86;&#x4E00;&#x500B;&#x5927;&#x5927;&#x8A62;&#x554F;Core Data&#x7684;&#x7D30;&#x7BC0;&#xFF0C;&#x60F3;&#x4E0D;&#x5230;&#x4ED6;&#x76F4;&#x63A5;&#x8DDF;&#x6211;&#x8AAA;&#x4ED6;&#x53EA;&#x6709;&#x7528;Realm&#x5F9E;&#x4F86;&#x6C92;&#x7528;&#x904E;Core Data&#xFF0C;&#x9019;&#x5C31;&#x662F;&#x6642;&#x4EE3;&#x7684;&#x773C;&#x6DDA;&#x55CE;QQ</p><h2 id="references">References</h2><p><a href>Core Data Programming Guide: Persistent Store Types and Behaviors</a>(https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/PersistentStoreFeatures.html)</p><p><a href>iOS 10 Core Data In Memory Store for Unit Tests - Stack Overflow</a>(https://stackoverflow.com/questions/39004864/ios-10-core-data-in-memory-store-for-unit-tests)</p><p><a href>Data Models and Model Objects &#xB7; objc.io</a>(https://www.objc.io/issues/4-core-data/core-data-models-and-model-objects/#data-model)</p><p><a href>Real-World Testing with XCTest &#xB7; objc.io</a>(https://www.objc.io/issues/15-testing/xctest/#why-we-are-testing)</p><p><a href>Easier Core Data Setup with Persistent Containers</a>(https://useyourloaf.com/blog/easier-core-data-setup-with-persistent-containers/)</p><p><a href>TestDouble</a>(https://martinfowler.com/bliki/TestDouble.html)</p>]]></content:encoded></item><item><title><![CDATA[歡迎來到真實世界 - 原來是那個傳說中的 MVVM 阿]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/clock-mechanism-pve479c.jpg" class="kg-image" alt="By NomadSoul1" loading="lazy"></figure><p>&#x597D;&#x4E0D;&#x5BB9;&#x6613;&#x4F86;&#x5230;&#x4E86;&#x7E8C;&#x4F5C;&#x7684;&#x7B2C;&#x4E09;&#x96C6;&#xFF0C;&#x5C31;&#x9019;&#x6A23;&#x4EE5;&#x8FD1;&#x4E4E;&#x4F11;&#x520A;&#x822C;&#x7684;&#x901F;&#x5EA6;&#xFF0C;&#x4E5F;&#x5BEB;&#x4E86;&#x4E09;&#x7BC7;&#x9577;&#x7BC7;&#x4E86;&#x3002;&#x96D6;&#x7136;&#x5C0D;&#x5F88;&#x591A;&#x9AD8;&#x624B;&#x524D;&#x8F29;&#x4F86;&#x8AAA;&#xFF0C;&#x9019;&#x4E9B;</p>]]></description><link>https://huangshihting.works/blog/huan-ying-lai-dao-zhen-shi-shi-jie-yuan-lai-shi-na-ge-chuan-shuo-zhong-de-mvvm-a/</link><guid isPermaLink="false">61f29fb856cf0e0001441859</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Wed, 04 Oct 2017 15:00:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/clock-mechanism-pve479c.jpg" class="kg-image" alt="By NomadSoul1" loading="lazy"></figure><p>&#x597D;&#x4E0D;&#x5BB9;&#x6613;&#x4F86;&#x5230;&#x4E86;&#x7E8C;&#x4F5C;&#x7684;&#x7B2C;&#x4E09;&#x96C6;&#xFF0C;&#x5C31;&#x9019;&#x6A23;&#x4EE5;&#x8FD1;&#x4E4E;&#x4F11;&#x520A;&#x822C;&#x7684;&#x901F;&#x5EA6;&#xFF0C;&#x4E5F;&#x5BEB;&#x4E86;&#x4E09;&#x7BC7;&#x9577;&#x7BC7;&#x4E86;&#x3002;&#x96D6;&#x7136;&#x5C0D;&#x5F88;&#x591A;&#x9AD8;&#x624B;&#x524D;&#x8F29;&#x4F86;&#x8AAA;&#xFF0C;&#x9019;&#x4E9B;&#x90FD;&#x662F;&#x975E;&#x5E38;&#x57FA;&#x790E;&#x7684;&#x6771;&#x897F;&#xFF0C;&#x4F46;&#x5C0F;&#x86C7;&#x5728;&#x8DCC;&#x8DCC;&#x649E;&#x649E;&#x4E86;&#x8A31;&#x4E45;(&#x9084;&#x5728;&#x8DCC;)&#x5F8C;&#xFF0C;&#x89BA;&#x5F97;&#x6709;&#x4E9B;&#x6771;&#x897F;&#x9084;&#x662F;&#x81EA;&#x5DF1;&#x5BEB;&#x4E0B;&#x4F86;&#xFF0C;&#x53EF;&#x4EE5;&#x518D;&#x6B21;&#x91D0;&#x6E05;&#x81EA;&#x5DF1;&#x7684;&#x89C0;&#x5FF5;&#xFF0C;&#x4E5F;&#x5077;&#x5077;&#x5E0C;&#x671B;&#x80FD;&#x5920;&#x7372;&#x5F97;&#x9AD8;&#x624B;&#x6307;&#x9EDE;&#x6216;&#x662F;&#x52A0;&#x5165;&#x8A0E;&#x8AD6;&#xFF0C;&#x9019;&#x5C31;&#x662F;&#x908A;&#x7DE3;&#x4EBA;&#x53C3;&#x8207;&#x793E;&#x6703;&#x7684;&#x65B9;&#x5F0F;&#x963F;(&#x8DDF;&#x672C;&#x5C31;&#x53EA;&#x662F;&#x5077;&#x61F6;&#x5427;)&#x3002;</p><p>&#x8A71;&#x4E0D;&#x591A;&#x8AAA;(&#x5DF2;&#x7D93;&#x8AAA;&#x5F88;&#x591A;&#x4E86;&#xFF0C;&#x60F3;&#x770B;&#x96FB;&#x5F71;&#x5FC3;&#x5F97;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x8DF3;&#x5230;&#x6700;&#x5F8C;)&#xFF0C;&#x5C31;&#x8B93;&#x6211;&#x5011;&#x9032;&#x5165;&#x4ECA;&#x5929;&#x7684;&#x4E3B;&#x984C;&#x3002;</p><p>&#x9019;&#x7BC7;&#x6211;&#x5011;&#x8981;&#x4F86;&#x8AC7;&#x8AC7;&#x958B;&#x767C;&#x4E0A;&#x66F4;&#x8CBC;&#x8FD1;&#x5BE6;&#x52D9;&#x7684;&#x90E8;&#x4EFD;&#xFF1A;&#x5982;&#x4F55;&#x8A2D;&#x8A08;&#x4E00;&#x500B;&#x597D;&#x7684;&#x8EDF;&#x9AD4;&#x67B6;&#x69CB;&#xFF0C;&#x4EE5;&#x53CA;&#x5982;&#x4F55;&#x6E2C;&#x8A66;&#x5B83;&#x3002;&#x5728;iOS&#x958B;&#x767C;&#x904E;&#x7A0B;&#x4E2D;&#xFF0C;&#x5982;&#x679C;&#x662F;&#x6BD4;&#x8F03;&#x5927;&#x578B;&#x7684;app&#xFF0C;&#x901A;&#x5E38;&#x8907;&#x96DC;&#x5EA6;&#x90FD;&#x975E;&#x5E38;&#x9AD8;&#xFF0C;&#x800C;&#x4E14;&#x624B;&#x6A5F;&#x958B;&#x767C;&#x6240;&#x9700;&#x8981;&#x67B6;&#x69CB;&#x7684;&#x6771;&#x897F;&#xFF0C;&#x5FC5;&#x9808;&#x8981;&#x878D;&#x5408;&#x524D;&#x5F8C;&#x7AEF;&#x7684;&#x77E5;&#x8B58;&#xFF0C;&#x5F9E;&#x8DDF;&#x4F7F;&#x7528;&#x8005;&#x7B2C;&#x4E00;&#x7DDA;&#x63A5;&#x89F8;&#x7684;UI&#xFF0C;&#x5230;&#x624B;&#x6A5F;&#x5E95;&#x5C64;&#x7684;&#x8CC7;&#x6599;&#x5EAB;&#xFF0C;&#x90FD;&#x5FC5;&#x9808;&#x900F;&#x904E;&#x4F60;&#x7684;code&#x4F86;&#x9023;&#x63A5;&#x8DDF;&#x5354;&#x8ABF;&#x3002;&#x9019;&#x500B;&#x67B6;&#x69CB;&#x597D;&#x4E0D;&#x597D;&#x8B80;&#x3001;&#x597D;&#x4E0D;&#x597D;&#x7DAD;&#x8B77;&#x3001;&#x597D;&#x4E0D;&#x597D;&#x6E2C;&#x8A66;&#xFF0C;&#x5C31;&#x6703;&#x662F;&#x6574;&#x500B;&#x958B;&#x767C;&#x7684;&#x91CD;&#x9EDE;&#x4E86;&#xFF0C;&#x5982;&#x679C;&#x9019;&#x500B;&#x67B6;&#x69CB;&#x4E0D;&#x662F;&#x5F88;&#x597D;&#xFF0C;&#x63A5;&#x624B;&#x7684;&#x4EBA;&#x6216;&#x5408;&#x4F5C;&#x7684;&#x4EBA;&#x7121;&#x6CD5;&#x5FEB;&#x901F;&#x7406;&#x89E3;&#xFF0C;&#x5C31;&#x9023;&#x4F60;&#x81EA;&#x5DF1;&#x6709;&#x6642;&#x5019;&#x90FD;&#x770B;&#x4E0D;&#x592A;&#x61C2;&#xFF0C;&#x90A3;&#x672A;&#x4F86;&#x67D0;&#x4E00;&#x5929;&#x4F60;&#x4E00;&#x5B9A;&#x6389;&#x9032;&#x4F60;&#x81EA;&#x5DF1;&#x6316;&#x51FA;&#x4F86;&#x7684;&#x5927;&#x5751;&#x88E1;(&#x5C0D;&#xFF0C;&#x5C0F;&#x86C7;&#x6211;&#x9084;&#x5728;&#x6211;&#x6316;&#x7684;&#x5751;&#x88E1;)&#x3002;</p><p>&#x8B1B;&#x67B6;&#x69CB;&#x6216;&#x8A31;&#x6709;&#x9EDE;&#x62BD;&#x8C61;&#xFF0C;&#x8981;&#x628A;&#x65E2;&#x6709;&#x7684;&#x67B6;&#x69CB;&#x6CD5;&#x5247;&#x5957;&#x5230;&#x81EA;&#x5DF1;&#x7684;&#x7A0B;&#x5F0F;&#x4E2D;&#x4E5F;&#x4E0D;&#x662F;&#x4E00;&#x5929;&#x5169;&#x5929;&#x7684;&#x4E8B;&#x60C5;&#xFF0C;&#x4F46;&#x6709;&#x500B;&#x597D;&#x65B9;&#x6CD5;&#x6216;&#x8A31;&#x53EF;&#x4EE5;&#x8A66;&#x4E00;&#x4E0B;&#xFF0C;&#x5F9E;&#x73FE;&#x5728;&#x958B;&#x59CB;&#xFF0C;&#x4F60;&#x53EF;&#x4EE5;&#x8A66;&#x8457;&#x57F9;&#x990A;&#x81EA;&#x5DF1;&#x7684;&#x6E2C;&#x8A66;&#x8166;&#x3002;&#x751A;&#x9EBC;&#x662F;&#x6E2C;&#x8A66;&#x8166;&#xFF1F;&#x5C31;&#x662F;&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x6240;&#x8981;&#x505A;&#x7684;&#x4E8B;&#x60C5;&#xFF0C;&#x6211;&#x6240;&#x8981;&#x505A;&#x7684;&#x6539;&#x8B8A;&#xFF0C;&#x90FD;&#x662F;</p><p><strong>&#x70BA;&#x4E86;&#x8981;&#x8B93;&#x6E2C;&#x8A66;&#x66F4;&#x5BB9;&#x6613;</strong>&#x3002;</p><p>&#x4F60;&#x5F88;&#x96E3;&#x60F3;&#x50CF;&#x600E;&#x6A23;&#x7684;&#x7A0B;&#x5F0F;&#x662F;&#x4E7E;&#x6DE8;&#x7684;&#x7A0B;&#x5F0F;&#xFF0C;&#x7562;&#x7ADF;&#x8EDF;&#x9AD4;&#x958B;&#x767C;&#x7684;&#x6CD5;&#x5247;&#x5F88;&#x591A;&#xFF0C;&#x5149;&#x662F;&#x8981;&#x4E0D;&#x8981;&#x5BEB;&#x8A3B;&#x89E3;&#x5C31;&#x6709;&#x975E;&#x5E38;&#x591A;&#x8AAA;&#x6CD5;&#x4E86;&#xFF0C;&#x5C0D;&#x65BC;&#x50CF;&#x5C0F;&#x86C7;&#x4E00;&#x6A23;&#x8CC7;&#x6B77;&#x4E0D;&#x6DF1;&#x7684;&#x4EBA;&#x4F86;&#x8AAA;&#xFF0C;&#x8DDF;&#x672C;&#x80CC;&#x4E0D;&#x8D77;&#x4F86;&#x66F4;&#x4E0D;&#x7528;&#x8AAA;&#x6D3B;&#x7528;&#x4E86;&#x3002;&#x4F46;&#x5C08;&#x6CE8;&#x5728;&#x8B93;code&#x5BB9;&#x6613;&#x5BEB;&#x6E2C;&#x8A66;&#xFF0C;&#x5C31;&#x6C92;&#x6709;&#x90A3;&#x9EBC;&#x96E3;&#x4E86;&#xFF0C;&#x56E0;&#x70BA;&#x4F60;&#x4E00;&#x958B;&#x59CB;&#x8981;&#x5BEB;&#x6E2C;&#x8A66;&#xFF0C;&#x5C31;&#x6703;&#x767C;&#x73FE;</p><p>&#x310A;&#x3107;&#x3109;&#x6211;&#x7684;&#x7A0B;&#x5F0F;&#x8DDF;&#x672C;&#x7121;&#x6CD5;&#x5BEB;&#x6E2C;&#x8A66;&#x963F;&#x963F;&#x963F;&#xFF01;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/giphy-tumblr.gif" class="kg-image" alt="giphy-tumblr.gif" loading="lazy"></figure><p>&#x5F9E;&#x9019;&#x908A;&#x958B;&#x59CB;&#xFF0C;&#x4F60;&#x5C31;&#x6703;&#x53BB;&#x7814;&#x7A76;&#x600E;&#x6A23;decoupling&#xFF0C;&#x7814;&#x7A76;design pattern&#xFF0C;&#x7814;&#x7A76;&#x5404;&#x7A2E;&#x65E2;&#x6709;&#x7684;&#x67B6;&#x69CB;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x56E0;&#x70BA;&#x6559;&#x4E3B;&#x8AAA;&#x5B83;&#x597D;&#x7528;&#x5C31;&#x7528;&#xFF0C;&#x9019;&#x5C31;&#x662F;&#x597D;&#x7684;&#x958B;&#x59CB;&#x3002;&#x6240;&#x4EE5;&#x4F86;&#x8DDF;&#x6211;&#x8AAA;&#x4E00;&#x6B21;&#xFF0C;&#x611F;&#x6069;&#x3119;...&#x4E0D;&#x5C0D;&#xFF0C;&#x662F;&#x300C;&#x6211;&#x8981;&#x8B93;&#x6E2C;&#x8A66;&#x66F4;&#x5BB9;&#x6613;&#x300D;&#xFF01;</p><p>&#x95DC;&#x65BC;&#x9019;&#x500B;&#x67B6;&#x69CB;&#x6587;&#xFF0C;&#x6211;&#x6703;&#x628A;&#x6545;&#x4E8B;&#x62C6;&#x5206;&#x6210;&#x5169;&#x7BC7;&#x6587;&#x7AE0;&#xFF0C;&#x7B2C;&#x4E00;&#x7BC7;&#x6703;&#x8B1B;&#x5230;&#x4E00;&#x822C;&#x7684;Apple MVC&#x67B6;&#x69CB;&#x65E2;&#x6709;&#x7684;&#x554F;&#x984C;&#xFF0C;&#x9084;&#x6709;&#x6211;&#x5011;&#x8981;&#x600E;&#x6A23;&#x6539;&#x5584;&#x5B83;&#x3002;&#x7B2C;&#x4E8C;&#x7BC7;&#x5247;&#x662F;&#x6703;&#x8B1B;&#x5230;&#x5982;&#x4F55;&#x91DD;&#x5C0D;MVVM&#x7684;&#x67B6;&#x69CB;&#x4F86;&#x64B0;&#x5BEB;unit test&#x3002;</p><h2 id="tldr">TL;DR</h2><p>&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x88E1;&#xFF0C;&#x4F60;&#x53EF;&#x4EE5;&#x4E86;&#x89E3;&#x5230;&#xFF1A;</p><ul><li>Apple MVC&#x67B6;&#x69CB;&#x6240;&#x5E36;&#x4F86;&#x7684;&#x554F;&#x984C;</li><li>&#x5229;&#x7528;MVVM&#x4F86;&#x8A2D;&#x8A08;&#x66F4;&#x4E7E;&#x6DE8;&#x7684;&#x67B6;&#x69CB;</li><li>&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;MVVM App&#x7BC4;&#x4F8B;</li></ul><p>&#x540C;&#x6642;&#xFF0C;&#x4F60;&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x88E1;&#x5C07;<strong>&#x4E0D;&#x6703;</strong>&#x770B;&#x5230;&#xFF1A;</p><ul><li>MVC&#x3001;MVVM&#x3001;VIPER&#x7684;&#x6BD4;&#x8F03;</li><li>MVVM&#x6D17;&#x8166;&#x5927;&#x6703;</li><li>&#x842C;&#x80FD;&#x7684;&#x8EDF;&#x9AD4;&#x67B6;&#x69CB;</li></ul><p>&#x4EE5;&#x4E00;&#x500B;&#x8EDF;&#x9AD4;&#x958B;&#x767C;&#x8005;&#x4F86;&#x8AAA;&#xFF0C;&#x9664;&#x975E;&#x4F60;&#x505C;&#x6B62;&#x958B;&#x767C;(&#x6216;&#x505C;&#x6B62;&#x547C;&#x5438;)&#x4E86;&#xFF0C;&#x4E0D;&#x7136;&#x8EDF;&#x9AD4;&#x67B6;&#x69CB;&#x6C38;&#x9060;&#x90FD;&#x4E0D;&#x6703;&#x6709;&#x6700;&#x597D;&#x7684;&#x3001;&#x6700;&#x5B8C;&#x7F8E;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x5728;&#x958B;&#x767C;&#x7684;&#x904E;&#x7A0B;&#x4E2D;&#xFF0C;&#x4F60;&#x7E3D;&#x662F;&#x53EF;&#x4EE5;&#x627E;&#x5230;&#x66F4;&#x597D;&#x7684;&#x6A21;&#x5F0F;&#xFF0C;&#x7E3D;&#x662F;&#x6703;&#x5B78;&#x5230;&#x65B0;&#x7684;&#x65B9;&#x6CD5;&#xFF0C;&#x9019;&#x4E9B;&#x6771;&#x897F;&#x90FD;&#x53EF;&#x4EE5;&#x4E0D;&#x65B7;&#x8B93;&#x4F60;&#x7684;&#x67B6;&#x69CB;&#x66F4;&#x4E7E;&#x6DE8;&#x66F4;&#x597D;&#x61C2;&#xFF0C;&#x6240;&#x4EE5;&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x88E1;&#xFF0C;&#x91CD;&#x9EDE;&#x6703;&#x653E;&#x5728;<strong>&#x70BA;&#x751A;&#x9EBC;</strong>&#x8981;&#x4F7F;&#x7528;MVVM&#xFF0C;&#x5B83;&#x89E3;&#x6C7A;&#x4E86;&#x600E;&#x6A23;&#x7684;&#x554F;&#x984C;&#xFF0C;&#x76F8;&#x4FE1;&#x9019;&#x4E9B;&#x80CC;&#x5F8C;&#x7684;&#x8108;&#x7D61;&#xFF0C;&#x4E5F;&#x540C;&#x6642;&#x53EF;&#x4EE5;&#x5957;&#x7528;&#x5728;&#x5176;&#x5B83;&#x4E0D;&#x540C;&#x7684;&#x67B6;&#x69CB;&#x4E0A;&#xFF0C;MVVM&#x53EA;&#x662F;&#x4F60;&#x8DDF;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x63A5;&#x89F8;&#x7684;&#x8F09;&#x9AD4;&#x800C;&#x5DF2;(&#x786C;&#x8981;&#x5957;&#x9EDE;&#x96FB;&#x5F71;&#x5F0F;&#x7684;&#x5047;&#x54F2;&#x5B78;)&#x3002;</p><h2 id="apple-mvc">Apple MVC</h2><p>Apple&#x6240;&#x63D0;&#x5021;&#x7684;MVC(Model-View-Controller)&#xFF0C;&#x662F;&#x5728;iOS&#x958B;&#x767C;&#x904E;&#x7A0B;&#x4E2D;&#xFF0C;&#x7B2C;&#x4E00;&#x500B;&#x6703;&#x9047;&#x5230;&#x7684;&#x67B6;&#x69CB;&#x3002;&#x5728;&#x539F;&#x672C;&#x7684;MVC&#x4E4B;&#x4E2D;&#xFF0C;Model&#x4EE3;&#x8868;&#x8CC7;&#x6599;&#xFF0C;View&#x4EE3;&#x8868;&#x8996;&#x5716;&#xFF0C;Controller&#x5247;&#x662F;&#x8CA0;&#x8CAC;&#x5546;&#x696D;&#x908F;&#x8F2F;&#x3002;&#x9019;&#x4E09;&#x8005;&#x7684;&#x4E92;&#x52D5;&#x65B9;&#x5F0F;&#x5982;&#x4E0B;&#x5716;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/model_view_controller_2x.png" class="kg-image" alt loading="lazy"></figure><p>Controller&#x540C;&#x6642;&#x64C1;&#x6709;View&#x8DDF;Model&#xFF0C;&#x4E26;&#x4E14;&#x505A;&#x70BA;&#x7D71;&#x6574;&#x5169;&#x908A;&#x7684;&#x6A4B;&#x6A11;&#x7684;&#x89D2;&#x8272;&#x3002;&#x4F46;&#x6545;&#x4E8B;&#x4F86;&#x5230;&#x4E86;iOS&#x958B;&#x767C;&#xFF0C;&#x56E0;&#x70BA;View&#x89D2;&#x8272;&#x7279;&#x6B8A;&#x7684;&#x95DC;&#x4FC2;&#xFF0C;&#x539F;&#x672C;&#x7684;MVC&#x8B8A;&#x6210;&#x4E86;Model-ViewController+View&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/massivevc.png" class="kg-image" alt loading="lazy"></figure><p>ViewController&#x5305;&#x542B;&#x4E86;View&#xFF0C;&#x4E26;&#x4E14;&#x52A0;&#x5165;&#x4E86;&#x4E00;&#x4E9B;View&#x7684;life cycle&#x7684;&#x908F;&#x8F2F;&#x3002;&#x56E0;&#x70BA;UIViewController&#x5730;&#x4F4D;&#x7279;&#x5225;&#x7684;&#x95DC;&#x4FC2;&#xFF0C;View&#x8DDF;Controller&#x7684;code&#x90FD;&#x6703;&#x51FA;&#x73FE;&#x5728;UIViewController&#x88E1;&#x9762;&#xFF0C;&#x9019;&#x6A23;&#x6703;&#x9020;&#x6210;UIViewController&#x8B8A;&#x5F97;&#x76F8;&#x7576;&#x80A5;&#x5927;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x5927;&#x5BB6;&#x5E38;&#x8AAA;&#x7684;Massive View Controller&#x3002;&#x4E26;&#x4E14;&#x9019;&#x500B;UIViewController&#x5176;&#x5BE6;&#x5F88;&#x4E0D;&#x597D;&#x5BEB;Unit Test&#xFF0C;&#x56E0;&#x70BA;&#x4F60;&#x7684;Controller&#x908F;&#x8F2F;&#x8DDF;View&#x7D81;&#x5F97;&#x592A;&#x6DF1;&#x3002;&#x4F60;&#x5982;&#x679C;&#x60F3;&#x8981;&#x6E2C;&#x67D0;&#x500B;Controller&#x7684;&#x529F;&#x80FD;&#xFF0C;&#x5C31;&#x5FC5;&#x9808;&#x8981;mock&#x67D0;&#x500B;view&#x4EE5;&#x53CA;&#x5B83;&#x7684;life cycle&#xFF0C;&#x9019;&#x6A23;&#x662F;&#x5F88;&#x4E0D;&#x7B26;&#x5408;&#x7D93;&#x6FDF;&#x6548;&#x76CA;&#x7684;&#x3002;</p><p>&#x91DD;&#x5C0D;Apple MVC&#x7684;&#x554F;&#x984C;&#xFF0C;&#x7DB2;&#x8DEF;&#x4E0A;&#x5DF2;&#x7D93;&#x6709;&#x975E;&#x5E38;&#x591A;&#x7684;&#x8A0E;&#x8AD6;&#x8DDF;&#x89E3;&#x6CD5;&#xFF0C;&#x4E0D;&#x7BA1;&#x662F;&#x90A3;&#x4E00;&#x7A2E;&#x89E3;&#x6C7A;&#x8FA6;&#x6CD5;&#xFF0C;&#x4E3B;&#x8981;&#x7684;&#x65BD;&#x529B;&#x9EDE;&#xFF0C;&#x90FD;&#x662F;&#x628A;&#x904E;&#x591A;&#x7684;&#x908F;&#x8F2F;&#xFF0C;&#x5F9E;UIViewController&#x88E1;&#x9762;&#x5207;&#x5206;&#x51FA;&#x4F86;&#xFF0C;&#x4E26;&#x4E14;&#x8A2D;&#x8A08;&#x4E00;&#x500B;&#x4E7E;&#x6DE8;&#x7684;&#x67B6;&#x69CB;&#xFF0C;&#x8B93;&#x6240;&#x6709;&#x7684;&#x7269;&#x4EF6;&#x80FD;&#x5920;&#x76E1;&#x91CF;&#x9075;&#x5FAA;single responsibility&#x539F;&#x5247;&#xFF0C;&#x4E0D;&#x8981;&#x6709;&#x5206;&#x5DE5;&#x4E0D;&#x660E;&#x7684;&#x554F;&#x984C;&#x3002;&#x76EE;&#x524D;&#x5E38;&#x898B;&#x7684;&#x66FF;&#x4EE3;&#x67B6;&#x69CB;&#x6709;MVVM&#x3001;VIPER&#x5169;&#x7A2E;&#xFF0C;&#x90FD;&#x662F;&#x89E3;&#x6C7A;Massive View Controller&#x7684;&#x597D;&#x65B9;&#x6CD5;&#xFF0C;&#x4E5F;&#x90FD;&#x6709;&#x5404;&#x81EA;&#x7684;&#x512A;&#x7F3A;&#x9EDE;&#x3002;&#x56E0;&#x70BA;MVVM&#x6BD4;&#x8F03;&#x597D;&#x4E0A;&#x624B;&#xFF0C;&#x4E5F;&#x6BD4;&#x8F03;&#x80FD;&#x5920;&#x62FF;&#x4F86;&#x89E3;&#x91CB;&#x5207;&#x5206;&#x6B0A;&#x8CAC;&#x7684;&#x6B65;&#x9A5F;&#xFF0C;&#x6240;&#x4EE5;&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x5011;&#x6703;&#x4EE5;MVVM&#x70BA;&#x4E3B;&#xFF0C;&#x4ECB;&#x7D39;MVVM&#x4EE5;&#x53CA;&#x600E;&#x6A23;&#x62FF;MVVM&#x4F86;&#x89E3;&#x6C7A;MVC&#x7684;&#x554F;&#x984C;&#x3002;</p><h2 id="mvvmmodelviewviewmodel">MVVM - Model - View - ViewModel</h2><p>MVVM&#x7684;&#x6982;&#x5FF5;&#x6700;&#x65E9;&#x61C9;&#x8A72;&#x662F;&#x5728;2005&#x5E74;&#x7531;Microsoft&#x7684;<a href="https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/">John Gossman</a>&#x63D0;&#x51FA;&#x4F86;&#x7684;&#xFF0C;&#x5B83;&#x7684;&#x6982;&#x5FF5;&#x662F;&#xFF0C;&#x6574;&#x500B;&#x6A21;&#x7D44;&#x6703;&#x62C6;&#x5206;&#x6210;&#x4E09;&#x500B;&#x90E8;&#x4EFD;&#xFF0C;View&#x3001;ViewModel&#x3001;Model&#xFF0C;&#x5176;&#x4E2D;View&#x7684;&#x89D2;&#x8272;&#x5C31;&#x662F;&#x55AE;&#x7D14;&#x7684;&#x8996;&#x89BA;&#x5143;&#x4EF6;&#xFF0C;&#x50CF;&#x662F;&#x6309;&#x9215;&#x3001;&#x6587;&#x5B57;&#x6A19;&#x7C64;&#x7B49;&#x7B49;&#xFF0C;&#x5728;View&#x88E1;&#x9762;&#x4E0D;&#x6703;&#x6709;&#x908F;&#x8F2F;&#x3001;&#x72C0;&#x614B;&#x7B49;&#x7B49;&#xFF0C;&#x55AE;&#x7D14;&#x5C31;&#x662F;&#x500B;&#x5448;&#x73FE;&#x8CC7;&#x6599;&#x7684;&#x5143;&#x4EF6;&#x3002;&#x800C;&#x8981;&#x8B93;View&#x5448;&#x73FE;&#x8CC7;&#x6599;&#xFF0C;&#x6700;&#x76F4;&#x89BA;&#x7684;&#x65B9;&#x5F0F;&#xFF0C;&#x5C31;&#x662F;&#x628A;View&#x8DDF;Model&#x505A;&#x7D81;&#x5B9A;&#xFF0C;&#x8B93;View&#x7684;&#x5143;&#x4EF6;&#x8DDF;&#x8457;Model&#x4E00;&#x8D77;&#x505A;&#x8B8A;&#x5316;&#x3002;&#x4F46;&#x9019;&#x6A23;&#x6703;&#x6709;&#x500B;&#x554F;&#x984C;&#xFF0C;&#x5C31;&#x662F;&#x901A;&#x5E38;Model&#x4F86;&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E0D;&#x662F;&#x7C21;&#x55AE;&#x5C31;&#x80FD;&#x8F49;&#x63DB;&#x6210;View&#x7684;&#x6A23;&#x5F0F;&#x7684;&#xFF0C;&#x9019;&#x6642;&#x5019;&#x5C31;&#x9700;&#x8981;&#x6709;&#x500B;&#x7269;&#x4EF6;&#xFF0C;&#x4ECB;&#x5728;View&#x8DDF;Model&#x7684;&#x4E2D;&#x9593;&#xFF0C;&#x9019;&#x500B;&#x7269;&#x4EF6;&#x6703;&#x638C;&#x7BA1;&#x9019;&#x4E9B;&#x8DDF;View&#x9AD8;&#x5EA6;&#x76F8;&#x95DC;&#x7684;&#x908F;&#x8F2F;&#x7684;&#x64CD;&#x4F5C;&#xFF0C;&#x50CF;&#x662F;&#x8F49;&#x63DB;Date&#x7269;&#x4EF6;&#x8B8A;&#x6210;&#x4EBA;&#x770B;&#x5F97;&#x61C2;&#x7684;&#x6587;&#x5B57;&#x683C;&#x5F0F;&#x7B49;&#xFF0C;&#x7A31;&#x4E4B;&#x70BA;ViewModel&#x3002;&#x4E0A;&#x9762;&#x7684;&#x6982;&#x5FF5;&#x53EF;&#x4EE5;&#x756B;&#x6210;&#x9019;&#x6A23;&#x7684;&#x8CC7;&#x6599;&#x6D41;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/mvvm-basic.png" class="kg-image" alt loading="lazy"></figure><p>&#x6D41;&#x7A0B;&#x4E0A;&#xFF0C;ViewModel&#x6703;&#x5F9E;Model&#x53D6;&#x5230;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x8CC7;&#x6599;&#x6574;&#x7406;&#x597D;&#x6210;&#x70BA;&#x65B9;&#x4FBF;&#x986F;&#x793A;&#x7684;&#x6A23;&#x5B50;&#xFF0C;&#x800C;View&#x4E00;&#x770B;&#x5230;ViewModel&#x7684;&#x8CC7;&#x6599;&#x6709;&#x66F4;&#x65B0;&#xFF0C;&#x5C31;&#x6703;&#x8DDF;&#x8457;&#x4E00;&#x8D77;&#x66F4;&#x65B0;&#xFF0C;&#x9019;&#x5C31;&#x662F;&#x4E00;&#x500B;&#x6700;&#x55AE;&#x7D14;&#x7684;MVVM&#x8CC7;&#x6599;&#x6D41;&#x3002;</p><p>&#x5728;iOS&#x958B;&#x767C;&#x4E0A;&#xFF0C;&#x4F9D;&#x7167;&#x4E0A;&#x8FF0;MVVM&#x7684;&#x5B9A;&#x7FA9;&#xFF0C;UIViewController&#x8B8A;&#x6210;&#x4E00;&#x500B;&#x55AE;&#x7D14;&#x7684;View&#xFF0C;&#x800C;&#x6211;&#x5011;&#x6703;&#x53E6;&#x5916;&#x7522;&#x751F;&#x4E00;&#x500B;ViewModel&#x4F86;&#x8CA0;&#x8CAC;presentational logic&#x8DDF;&#x90E8;&#x4EFD;&#x7684;controller logic&#x3002;&#x6240;&#x4EE5;&#x5728;&#x4F60;&#x7684;ViewController&#x88E1;&#x9762;&#xFF0C;&#x5C31;&#x53EA;&#x6703;&#x6709;&#xFF1A;</p><ol><li>View logic&#xFF0C;&#x6240;&#x6709;&#x8DDF;&#x5448;&#x73FE;&#x6709;&#x95DC;&#x7684;Code</li><li>&#x7D81;&#x5B9A;ViewModel</li></ol><p>&#x800C;&#x5728;ViewModel&#x88E1;&#x9762;&#xFF0C;&#x5247;&#x662F;&#x8CA0;&#x8CAC;&#x5169;&#x500B;&#x90E8;&#x4EFD;&#xFF1A;</p><ol><li>Controller logic&#xFF0C;&#x5982;pagination, error handling,&#x2026; etc</li><li>Presentation logic&#xFF0C;&#x63D0;&#x4F9B;&#x63A5;&#x53E3;&#x8B93;View&#x7D81;&#x5B9A;(binding)</li></ol><p>&#x958B;&#x767C;&#x4E0A;&#xFF0C;&#x4E00;&#x65E6;View&#x7D81;&#x5B9A;&#x597D;ViewModel&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x5728;&#x64B0;&#x5BEB;&#x5546;&#x696D;&#x908F;&#x8F2F;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x4E0D;&#x7528;&#x7BA1;&#x5305;&#x62EC;&#x52D5;&#x756B;&#x3001;&#x8F49;&#x5834;&#x3001;main thread&#x7B49;&#x7B49;&#x8DDF;View&#x76F8;&#x95DC;&#x7684;&#x554F;&#x984C;&#xFF0C;&#x56E0;&#x70BA;&#x5206;&#x5DE5;&#x660E;&#x78BA;&#x6240;&#x4EE5;&#x5C31;&#x4E0D;&#x6703;&#x6709;&#x5BEB;&#x8D77;&#x4F86;&#x7D81;&#x624B;&#x7D81;&#x8173;&#x7684;&#x611F;&#x89BA;&#x3002;&#x66F4;&#x68D2;&#x7684;&#x662F;&#xFF0C;&#x4E26;&#x4E14;&#x56E0;&#x70BA;ViewModel&#x662F;&#x4E00;&#x500B;&#x55AE;&#x7D14;&#x7684;&#x3001;&#x6C92;&#x6709;&#x76F8;&#x4F9D;&#x65BC;View&#x7684;&#x7269;&#x4EF6;&#xFF0C;&#x6240;&#x4EE5;&#x8981;&#x505A;&#x6E2C;&#x8A66;&#x7C21;&#x55AE;&#x591A;&#x4E86;&#xFF01;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/giphy-downsized.gif" class="kg-image" alt="giphy-downsized.gif" loading="lazy"></figure><p>&#x5728;&#x6587;&#x672B;&#x6211;&#x5011;&#x6703;&#x8A0E;&#x8AD6;&#x9019;&#x500B;&#x5176;&#x5BE6;&#x4E5F;&#x8EAB;&#x517C;&#x591A;&#x8077;&#x7684;ViewModel&#x5230;&#x5E95;&#x6709;&#x751A;&#x9EBC;&#x554F;&#x984C;&#xFF0C;&#x5C31;&#x8B93;&#x6211;&#x5011;&#x7E7C;&#x7E8C;&#x770B;&#x4E0B;&#x53BB;&#xFF5E;</p><h1 id="a-simple-gallery-appmvc">A simple gallery app - MVC</h1><p>&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x7528;&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;&#x4F8B;&#x5B50;&#xFF0C;&#x4F86;&#x8B93;&#x5927;&#x5BB6;&#x4E86;&#x89E3;&#x600E;&#x9EBC;&#x5F9E;MVC&#x8F49;&#x63DB;&#x5230;MVVM&#x3002;</p><p>&#x9019;&#x662F;&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;App&#xFF0C;&#x5177;&#x6709;&#x4E0B;&#x9762;&#x5169;&#x500B;&#x529F;&#x80FD;&#xFF1A;</p><ol><li>&#x6703;&#x5F9E;500px API&#x6293;&#x53D6;&#x71B1;&#x9580;&#x76F8;&#x7247;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x76F8;&#x7247;&#x6392;&#x6210;&#x5217;&#x8868;show&#x51FA;&#x4F86;&#xFF0C;&#x6BCF;&#x5F35;&#x76F8;&#x7247;&#x90FD;&#x6703;&#x986F;&#x793A;&#x6A19;&#x984C;&#x3001;&#x63CF;&#x8FF0;&#x3001;&#x8DDF;&#x62CD;&#x651D;&#x65E5;&#x671F;&#x3002;</li><li>&#x5982;&#x679C;&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x9078;&#x4E86;&#x975E;&#x8CE3;&#x54C1;&#xFF0C;app&#x5C31;&#x4E0D;&#x6703;&#x8B93;&#x4F7F;&#x7528;&#x8005;&#x9032;&#x5230;&#x4E0B;&#x4E00;&#x9801;&#xFF0C;&#x4E26;&#x4E14;&#x8DF3;&#x51FA;&#x932F;&#x8AA4;&#x8A0A;&#x606F;&#x3002;</li></ol><p>&#x8ACB;&#x770B;&#x4EE5;&#x4E0B;&#x793A;&#x610F;&#x5716;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/giphy-1.gif" class="kg-image" alt="giphy (1).gif" loading="lazy"></figure><p>&#x5728;&#x9019;&#x500B;app&#x88E1;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x6709;&#x4E00;&#x500B;Model&#xFF0C;&#x53EB;Photo&#xFF0C;&#x4EE3;&#x8868;&#x7684;&#x5C31;&#x662F;&#x4E00;&#x5F35;&#x7167;&#x7247;&#xFF0C;&#x9019;&#x500B;Model&#x6703;&#x8DDF;JSON&#x4E0A;&#x62FF;&#x5230;&#x7684;&#x8CC7;&#x6599;&#x683C;&#x5F0F;&#x4E00;&#x6A23;&#xFF0C;&#x5982;&#x4E0B;&#xFF1A;</p><p>https://gist.github.com/koromiko/14daab5f0e8bad28447dffcb4486c7f2<br>&#x800C;&#x6211;&#x5011;&#x900F;&#x904E;&#x4E00;&#x500B;APIService&#x7269;&#x4EF6;&#xFF0C;&#x4E0A;&#x7DB2;&#x53BB;&#x6293;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x8CC7;&#x6599;&#x8F49;&#x6210;Photo&#x7269;&#x4EF6;&#x4F9B;ViewController&#x4F7F;&#x7528;&#xFF0C;&#x4E26;&#x4E14;&#x7531;&#x4E00;&#x500B;activity indicator&#x4F86;&#x986F;&#x793A;&#x8B80;&#x53D6;&#x4E2D;&#x7684;&#x8CC7;&#x8A0A;&#xFF0C;&#x9019;&#x500B;&#x4E8B;&#x4EF6;&#x767C;&#x751F;&#x5728;ViewDidLoad&#x88E1;&#x9762;&#xFF1A;</p><p>https://gist.github.com/koromiko/4f5d9eb32501ee0bf450548ce95d28e6<br>&#x800C;&#x9019;&#x500B;tableView&#x7684;data source&#xFF0C;&#x4E5F;&#x6703;&#x5BEB;&#x5728;&#x9019;&#x500B;VIewController&#x88E1;&#x9762;&#xFF0C;&#x5982;&#x4E0B;&#xFF1A;</p><p>https://gist.github.com/koromiko/f905fd49023907440d2da12753063521<br>&#x9019;&#x500B;tableView&#x7684;&#x6578;&#x91CF;&#x5C31;&#x7B49;&#x540C;&#x65BC;&#x6293;&#x4E0B;&#x4F86;&#x7684;Photo&#x6578;&#x91CF;&#xFF0C;&#x4E26;&#x4E14;&#x5728;reuse cell&#x6642;&#xFF0C;&#x53D6;&#x5C0D;&#x61C9;&#x7684;photo&#x7269;&#x4EF6;&#xFF0C;&#x6253;&#x5305;&#x6210;&#x5C0D;&#x61C9;&#x7684;&#x683C;&#x5F0F;&#xFF0C;&#x8A2D;&#x5B9A;&#x5230;cell&#x4E0A;&#x9762;&#x3002;&#x5728;&#x9019;&#x88E1;&#xFF0C;&#x56E0;&#x70BA;Date&#x7269;&#x4EF6;&#x7121;&#x6CD5;&#x76F4;&#x63A5;&#x986F;&#x793A;&#x5728;View&#x4E0A;&#x9762;&#xFF0C;&#x9700;&#x8981;&#x8B8A;&#x6210;&#x201D;yyyy-MM-dd&#x201D;&#x9019;&#x6A23;&#x7684;&#x683C;&#x5F0F;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x8981;&#x5728;&#x6307;&#x5B9A;&#x8CC7;&#x6599;&#x5230;UILabel&#x4E0A;&#x4E4B;&#x524D;&#x505A;&#x8F49;&#x63DB;&#xFF0C;&#x628A;Date&#x8F49;&#x6210;&#x5B57;&#x4E32;&#x8B93;label&#x53EF;&#x4EE5;&#x6B63;&#x78BA;&#x986F;&#x793A;&#x3002;</p><p>&#x800C;&#x8DDF;&#x4F7F;&#x7528;&#x8005;&#x4E92;&#x52D5;&#x7684;delegate&#x90E8;&#x4EFD;&#xFF0C;&#x5247;&#x662F;&#x9019;&#x6A23;&#xFF1A;</p><p>https://gist.github.com/koromiko/7cd42d963f3d02bf88faac630a67705b<br>&#x5728;<code>func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -&gt; IndexPath?</code>&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x5148;&#x53BB;&#x5224;&#x65B7;&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x9078;&#x7684;&#x7167;&#x7247;&#xFF0C;&#x5982;&#x679C;&#x9EDE;&#x9078;&#x7684;&#x7167;&#x7247;&#x662F;for sale&#x7684;&#xFF0C;&#x5C31;&#x8A18;&#x9304;&#x4F7F;&#x7528;&#x8005;&#x9EDE;&#x9078;&#x7684;indexPath&#x4F9B;segue&#x4F7F;&#x7528;&#x3002;&#x5982;&#x679C;&#x9EDE;&#x9078;&#x7684;&#x4E0D;&#x662F;for sale&#xFF0C;&#x5C31;&#x8DF3;&#x51FA;&#x4E00;&#x500B;alert&#xFF0C;&#x8AAA;&#x9019;&#x662F;&#x975E;&#x8CE3;&#x54C1;&#xFF0C;&#x4E26;&#x4E14;&#x56DE;&#x50B3;nil&#xFF0C;&#x8B93;segue&#x4E0D;&#x8981;&#x767C;&#x751F;&#x3002;</p><p>&#x4EE5;&#x4E0A;&#x5C31;&#x662F;&#x4E00;&#x500B;&#x6700;&#x7C21;&#x55AE;&#x7684;app&#xFF0C;&#x8A73;&#x7D30;&#x7684;&#x539F;&#x59CB;&#x78BC;&#x53EF;&#x4EE5;&#x53C3;&#x7167;<a href="https://github.com/koromiko/Tutorial/blob/MVC/MVVMPlayground/MVVMPlayground/Module/PhotoList/PhotoListViewController.swift">&#x9019;&#x88E1;</a>(tag::&#x201D;MVC&#x201D;)&#x3002;</p><p>&#x9019;&#x662F;&#x57FA;&#x672C;&#x7684;Apple MVC&#x67B6;&#x69CB;&#xFF0C;&#x4E5F;&#x662F;&#x5404;&#x7A2E;&#x6559;&#x5B78;&#x4E2D;&#x5E38;&#x898B;&#x7684;&#x7BC4;&#x4F8B;&#xFF0C;&#x6253;&#x5B57;&#x5FEB;&#x4E00;&#x9EDE;&#x7684;&#x4EBA;&#x61C9;&#x8A72;&#x4E0D;&#x7528;&#x5E7E;&#x5206;&#x9418;&#x5C31;&#x53EF;&#x4EE5;&#x523B;&#x51FA;&#x9019;&#x6A23;&#x7684;&#x6771;&#x897F;&#x4F86;&#x3002;&#x4F46;&#x9019;&#x6771;&#x897F;&#x6709;&#x751A;&#x9EBC;&#x554F;&#x984C;&#xFF1F;&#x5728;&#x9019;&#x500B;MVC&#x88E1;&#x9762;&#xFF0C;&#x540C;&#x6642;&#x4E5F;&#x6709;&#x50CF;&#x662F;activity indicator&#x3001;tableview&#x51FA;&#x73FE;&#x6216;&#x6D88;&#x5931;&#x7B49;&#x7B49;&#x7684;&#x908F;&#x8F2F;(Presnetation logic)&#xFF0C;&#x4E5F;&#x6709;&#x4E0A;&#x7DB2;&#x53D6;&#x8CC7;&#x6599;&#x7684;&#x908F;&#x8F2F;(Controller logic)&#xFF0C;&#x52A0;&#x4E0A;View&#x8DDF;&#x5B83;&#x5011;&#x7684;life cycle&#x6574;&#x500B;&#x88AB;&#x7D81;&#x5728;UIViewController&#x88E1;&#x9762;&#xFF0C;&#x6240;&#x4EE5;&#x9019;&#x500B;ViewController&#x7684;&#x89D2;&#x8272;&#x8B8A;&#x5F97;&#x6709;&#x9EDE;&#x6DF7;&#x4E82;&#x3002;&#x66F4;&#x9EBB;&#x7169;&#x7684;&#x662F;&#xFF0C;&#x9019;&#x500B;ViewController&#x662F;&#x5F88;&#x96E3;&#x88AB;&#x6E2C;&#x8A66;&#x7684;&#xFF01;&#x9664;&#x975E;Mock&#x6574;&#x5F35;tableView&#x8207;&#x5B83;&#x7684;cell&#xFF0C;&#x4E0D;&#x7136;&#x6211;&#x5011;&#x6C92;&#x8FA6;&#x6CD5;&#x77E5;&#x9053;date&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6B63;&#x78BA;&#x5730;&#x88AB;&#x8F49;&#x6210;&#x8A72;&#x6709;&#x7684;&#x6A23;&#x5B50;&#xFF0C;&#x6211;&#x5011;&#x4E5F;&#x9700;&#x8981;mock activity indicator&#xFF0C;&#x624D;&#x80FD;&#x5920;&#x77E5;&#x9053;loading&#x7684;&#x72C0;&#x614B;&#x662F;&#x4E0D;&#x662F;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x5C0D;&#x61C9;&#xFF0C;&#x9019;&#x500B;&#x6E2C;&#x8A66;&#x5BEB;&#x8D77;&#x4F86;&#x6703;&#x975E;&#x5E38;&#x53EF;&#x6015;&#x3002;</p><p>&#x70BA;&#x4E86;&#x8B93;&#x6E2C;&#x8A66;&#x8B8A;&#x7684;&#x66F4;&#x5BB9;&#x6613;&#xFF0C;&#x8B93;&#x6211;&#x5011;&#x52D5;&#x624B;&#x4F86;&#x6539;&#x8B8A;&#x9019;&#x4E00;&#x5207;&#x3002;</p><h2 id="let%E2%80%99s-do-mvvm">Let&#x2019;s do MVVM</h2><p>&#x70BA;&#x4E86;&#x8981;&#x89E3;&#x6C7A;&#x4E0A;&#x9762;&#x9019;&#x4E9B;&#x554F;&#x984C;&#xFF0C;&#x6211;&#x5011;&#x7684;&#x9996;&#x8981;&#x4E4B;&#x52D9;&#x5C31;&#x662F;&#x8981;&#x6E05;&#x7406;ViewController&#xFF0C;&#x8B93;&#x90E8;&#x4EFD;&#x7684;&#x908F;&#x8F2F;&#x7368;&#x7ACB;&#x51FA;&#x4F86;&#xFF0C;&#x6210;&#x70BA;&#x4E00;&#x500B;&#x6709;&#x4E3B;&#x6B0A;&#x3001;&#x6709;&#x9818;&#x571F;&#x3001;&#x80FD;&#x81EA;&#x6C7A;&#x7684;&#x7269;&#x4EF6;&#xFF01;(&#x662F;&#x4E0D;&#x662F;&#x5F88;&#x503C;&#x5F97;&#x652F;&#x6301;) &#x56DE;&#x9867;&#x525B;&#x525B;MVVM&#x7684;&#x5B9A;&#x7FA9;&#xFF0C;&#x6211;&#x5011;&#x76EE;&#x524D;&#x7684;&#x4EFB;&#x52D9;&#x5C31;&#x662F;&#xFF1A;</p><ol><li>&#x628A;View&#x8DDF;ViewModel&#x505A;&#x7D81;&#x5B9A;</li><li>&#x628A;controller logic&#x8DDF;presentation logic&#x5F9E;ViewController&#x79FB;&#x5230;ViewModel</li></ol><p>&#x5148;&#x770B;&#x7D81;&#x5B9A;&#x7684;&#x90E8;&#x4EFD;&#xFF0C;&#x5728;&#x9801;&#x9762;&#x4E0A;&#xFF0C;&#x6211;&#x5011;&#x7684;View&#x4E0A;&#x6709;&#x5E7E;&#x500B;&#x4E3B;&#x8981;&#x5143;&#x4EF6;&#xFF1A;</p><ol><li>activity Indicator (loading/finish)</li><li>tableView (show/hide)</li><li>cells (title, description, created date)</li></ol><p>&#x5982;&#x679C;&#x6211;&#x5011;&#x628A;&#x9019;&#x4E9B;&#x5143;&#x4EF6;&#x7684;&#x8CC7;&#x6599;&#x8DDF;&#x72C0;&#x614B;&#x6574;&#x7406;&#x51FA;&#x4F86;&#xFF0C;&#x62BD;&#x8C61;&#x5316;&#x6210;&#x70BA;&#x4E00;&#x4E9B;ViewModel&#x7684;&#x63A5;&#x53E3;&#xFF0C;&#x5C31;&#x6703;&#x8B8A;&#x6210;&#x50CF;&#x4E0B;&#x5716;&#x9019;&#x6A23;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/mvvm-001.png" class="kg-image" alt loading="lazy"></figure><p>&#x6240;&#x6709;&#x7684;View&#x7684;&#x72C0;&#x614B;&#xFF0C;&#x90FD;&#x6709;&#x4ED6;&#x5011;&#x5C0D;&#x61C9;&#x7684;ViewModel properties&#xFF0C;&#x4E26;&#x4E14;&#x6BCF;&#x500B;cell&#x4E5F;&#x90FD;&#x6709;&#x76F8;&#x5C0D;&#x61C9;&#x7684;ViewModels&#xFF0C;&#x9019;&#x6A23;&#x5C31;&#x80FD;&#x5920;&#x78BA;&#x4FDD;View&#x7684;&#x9577;&#x76F8;&#x5C31;&#x662F;&#x6211;&#x5011;&#x5728;ViewModel&#x4E0A;&#x9762;&#x770B;&#x5230;&#x7684;&#x4E00;&#x6A23;&#x3002;</p><p>&#x90A3;&#x5BE6;&#x4F5C;&#x4E0A;&#x6211;&#x5011;&#x8981;&#x600E;&#x9EBC;&#x505A;&#x7D81;&#x5B9A;&#x5462;&#xFF1F;</p><h2 id="implement-the-binding-with-closure">Implement the Binding with Closure</h2><p>&#x5728;Swift&#x88E1;&#xFF0C;&#x8981;&#x505A;&#x5230;&#x8CC7;&#x6599;&#x7D81;&#x5B9A;&#xFF0C;&#x6709;&#x5E7E;&#x7A2E;&#x65B9;&#x6CD5;&#xFF1A;</p><ol><li>&#x7528;ObjC&#x7684;KVO pattern</li><li>&#x4F7F;&#x7528;FRP&#x5957;&#x4EF6;&#x5982;RxSwift&#x6216;ReactiveCocoa&#x63D0;&#x4F9B;&#x7684;binding&#x529F;&#x80FD;</li><li>&#x81EA;&#x5DF1;&#x5BE6;&#x4F5C;</li></ol><p>&#x4F7F;&#x7528;ObjC&#x7684;KVO(Key-Value Observer)&#x662F;&#x500B;&#x4E0D;&#x932F;&#x7684;&#x65B9;&#x6CD5;&#xFF0C;&#x4F46;&#x662F;&#x56E0;&#x70BA;KVO&#x672C;&#x8EAB;&#x5728;ObjC&#x88E1;&#x7279;&#x5225;&#x7684;&#x8A2D;&#x8A08;&#xFF0C;&#x6240;&#x6709;&#x66F4;&#x65B0;&#x6578;&#x503C;&#x90FD;&#x662F;&#x900F;&#x904E;&#x4E00;&#x500B;delegate function&#x4F86;&#x9054;&#x6210;&#xFF0C;&#x6240;&#x4EE5;&#x4F7F;&#x7528;KVO&#x5728;ViewController&#x88E1;&#x9762;&#x6703;&#x8B8A;&#x5F97;&#x6709;&#x9EDE;&#x8907;&#x96DC;&#xFF0C;&#x9019;&#x6A23;&#x5C31;&#x5931;&#x53BB;&#x6211;&#x5011;&#x60F3;&#x8981;&#x7C21;&#x5316;&#x7684;&#x610F;&#x7FA9;&#x4E86;&#x3002;&#x4F7F;&#x7528;FRP(Functional Reactive Programming)&#x63D0;&#x4F9B;&#x7684;binding&#x662F;&#x6700;&#x65B9;&#x4FBF;&#x7684;&#xFF0C;&#x4E00;&#x65E6;&#x5F15;&#x5165;&#x4E86;signal&#x8DDF;event&#x7684;&#x6982;&#x5FF5;&#x5F8C;&#xFF0C;View&#x8DDF;ViewModel&#x4E4B;&#x9593;&#x7684;&#x4E92;&#x52D5;&#x5C31;&#x6709;&#x7D71;&#x4E00;&#x4E14;&#x76F4;&#x89C0;&#x7684;&#x4F5C;&#x6CD5;&#xFF0C;&#x4E0D;&#x904E;&#x56E0;&#x70BA;FRP&#x662F;&#x4E00;&#x500B;&#x4E0D;&#x5C0F;&#x7684;&#x6982;&#x5FF5;&#xFF0C;&#x70BA;&#x4E86;&#x907F;&#x514D;&#x5931;&#x7126;&#x6240;&#x4EE5;&#x5728;&#x9019;&#x908A;&#x6211;&#x5011;&#x4E5F;&#x4E0D;&#x4F7F;&#x7528;&#x3002;&#x81EA;&#x5DF1;&#x5BE6;&#x4F5C;&#x7D81;&#x5B9A;&#x7684;&#x8A71;&#xFF0C;&#x6709;&#x4E0D;&#x5C11;&#x65B9;&#x6CD5;&#xFF0C;&#x50CF;&#x662F;<a href="http://five.agency/solving-the-binding-problem-with-swift/">&#x9019;&#x7BC7;</a>&#x5229;&#x7528;decorator pattern&#x4F86;&#x505A;&#x5230;&#x4E0D;&#x540C;&#x985E;&#x578B;&#x7684;&#x7269;&#x4EF6;&#x7D81;&#x5B9A;&#xFF0C;&#x5F88;&#x503C;&#x5F97;&#x6DF1;&#x5165;&#x7814;&#x7A76;&#x3002;</p><p>&#x5728;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#xFF0C;&#x6211;&#x5011;&#x9078;&#x64C7;&#x4E00;&#x500B;&#x66F4;&#x55AE;&#x7D14;&#x7684;&#x505A;&#x6CD5;&#xFF1A;&#x5229;&#x7528;Closure&#xFF0C;&#x8B93;ViewController&#x53BB;&#x7B49;&#x5F85;ViewModel&#x7684;&#x6539;&#x8B8A;&#xFF0C;&#x4F86;&#x89F8;&#x767C;View&#x7684;&#x66F4;&#x65B0;&#xFF0C;&#x9054;&#x5230;&#x7D81;&#x5B9A;&#x7684;&#x6548;&#x679C;&#x3002;</p><p>&#x5177;&#x9AD4;&#x4F86;&#x8AAA;&#xFF0C;&#x4E00;&#x500B;&#x5728;ViewModel&#x88E1;&#x9762;&#xFF0C;&#x5373;&#x5C07;&#x8DDF;View&#x7D81;&#x5B9A;&#x7684;property&#xFF0C;&#x6703;&#x9577;&#x9019;&#x500B;&#x6A23;&#x5B50;&#xFF1A;</p><p>https://gist.github.com/koromiko/9de5d10f919b572f5cc201fe5301826c<br>&#x5728;View&#x521D;&#x59CB;&#x5316;ViewModel&#x6642;&#xFF0C;&#x6703;&#x9806;&#x4FBF;&#x8A2D;&#x5B9A;&#x597D;viewModel.triggerVIewUpdate&#xFF1A;</p><p>https://gist.github.com/koromiko/d4704a89200444575624ab0dc5d2ff46<br>&#x6240;&#x4EE5;&#x6BCF;&#x7576;ViewModel&#x88E1;&#x9762;&#x7684;prop&#x66F4;&#x65B0;&#x6642;&#xFF0C;&#x90FD;&#x6703;&#x89F8;&#x767C;&#x9019;&#x500B;closure&#xFF0C;&#x9032;&#x800C;&#x8B93;View&#x505A;&#x67D0;&#x4E9B;&#x66F4;&#x65B0;&#xFF0C;&#x9019;&#x7A2E;&#x7D81;&#x5B9A;&#x5F88;&#x597D;&#x7406;&#x89E3;&#x4E26;&#x4E14;&#x7463;&#x788E;&#x7684;code&#x4E5F;&#x4E0D;&#x591A;&#xFF0C;&#x4E5F;&#x53EF;&#x4EE5;&#x5F88;&#x9748;&#x6D3B;&#x7684;&#x904B;&#x7528;&#x3002;</p><h2 id="interfaces-for-bindingviewmodel">Interfaces for binding - ViewModel</h2><p>&#x73FE;&#x5728;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x4F86;&#x5BEB;code&#x4E86;&#xFF01;&#x6211;&#x5011;&#x5148;&#x8A2D;&#x8A08;&#x51FA;&#x7C21;&#x55AE;&#x7684;PhotoListViewModel&#xFF0C;&#x5177;&#x6709;&#x63A5;&#x53E3;&#x5982;&#x4E0B;&#xFF1A;</p><p>https://gist.github.com/koromiko/97e95db0a30d9b6100a4b96f1a2d21f7<br>&#x6BCF;&#x500B;cell&#x7684;ViewModel&#xFF0C;&#x90FD;&#x88AB;&#x5B58;&#x5728;cellViewModels&#x88E1;&#x9762;&#xFF0C;&#x90FD;&#x53EF;&#x4EE5;&#x900F;&#x904E;getCellViewModel&#x4F86;&#x53D6;&#x5F97;&#xFF0C;cell&#x7684;&#x6578;&#x91CF;&#x5247;&#x662F;&#x900F;&#x904E;numberOfCells&#x4F86;&#x53D6;&#x5F97;&#xFF0C;&#x53EA;&#x8981;cellViewModels&#x9019;&#x500B;property&#x4E00;&#x66F4;&#x65B0;&#xFF0C;tableView&#x5C31;&#x6703;&#x8DDF;&#x8457;&#x91CD;&#x6574;&#x3002;&#x800C;&#x6BCF;&#x4E00;&#x500B;cellViewModel&#x6703;&#x9577;&#x5F97;&#x50CF;&#x4E0B;&#x9762;&#x9019;&#x6A23;&#xFF1A;</p><p>https://gist.github.com/koromiko/0ee1b101c605b0b83a0396549c14f8df<br>&#x9019;&#x500B;PhotoListCellViewModel&#x4EE3;&#x8868;&#x6BCF;&#x4E00;&#x500B;&#x5373;&#x5C07;&#x51FA;&#x73FE;&#x5728;cell&#x4E0A;&#x9762;&#x7684;&#x8CC7;&#x8A0A;&#xFF0C;&#x6240;&#x4EE5;cell&#x53EA;&#x8981;&#x7167;&#x8457;&#x4E0A;&#x9762;&#x7684;&#x8CC7;&#x6599;&#x5448;&#x73FE;&#x8996;&#x5716;&#x5C31;&#x597D;&#xFF0C;&#x4E0D;&#x7528;&#x505A;&#x4EFB;&#x4F55;&#x8F49;&#x63DB;&#x3002;</p><h2 id="bind-view-with-viewmodel">Bind View with ViewModel</h2><p>&#x6709;&#x4E86;&#x4E0A;&#x9762;&#x9019;&#x4E9B;&#x63A5;&#x53E3;&#xFF0C;&#x63A5;&#x8457;&#x6211;&#x5011;&#x5C31;&#x8981;&#x4F86;&#x64B0;&#x5BEB;ViewController&#x7684;&#x90E8;&#x4EFD;&#x3002;&#x9996;&#x5148;&#xFF0C;&#x5728;ViewDidLoad&#x5148;&#x6307;&#x5B9A;&#x597D;ViewModel&#x7684;closure&#xFF1A;</p><p>https://gist.github.com/koromiko/c4b463e33b58438214af090b72d1a22d<br>&#x91DD;&#x5C0D;tableView&#x7684;datasource&#xFF0C;&#x5247;&#x6539;&#x6210;&#x5F9E;PhotoListViewModel&#x62FF;&#x5230;PhotoListCellViewModel&#xFF0C;&#x5728;&#x5229;&#x7528;cellViewModel&#x4F86;&#x6307;&#x793A;cell&#x8981;&#x600E;&#x6A23;&#x5448;&#x73FE;&#xFF1A;</p><p>https://gist.github.com/koromiko/f25e866d209c7048544c4d64fe5251fb<br>&#x9019;&#x6A23;&#x8CC7;&#x6599;&#x6D41;&#x5C31;&#x8B8A;&#x6210;&#x4E86;&#xFF0C;ViewModel&#x4E00;&#x65E6;&#x6574;&#x7406;&#x597D;&#x8CC7;&#x6599;&#xFF0C;View&#x5C31;&#x6703;&#x53BB;&#x8DDF;ViewModel&#x62FF;&#x6574;&#x7406;&#x597D;&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x66F4;&#x65B0;&#x81EA;&#x5DF1;&#x4E26;&#x4E14;&#x986F;&#x73FE;&#x51FA;&#x4F86;&#x3002;&#x5B8C;&#x6574;&#x7684;&#x8CC7;&#x6599;&#x6D41;&#x6703;&#x9577;&#x5F97;&#x50CF;&#x4E0B;&#x5716;&#x4E00;&#x6A23;&#xFF1A;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/mvvm-001-1.png" class="kg-image" alt loading="lazy"></figure><h2 id="user-interactionview">User interaction - View</h2><p>&#x5982;&#x679C;&#x4F7F;&#x7528;&#x8005;&#x6709;&#x4E92;&#x52D5;&#x7684;&#x6642;&#x5019;&#x5462;&#xFF1F;&#x6211;&#x5011;&#x5728;ViewModel&#x88E1;&#x9762;&#xFF0C;&#x63D0;&#x4F9B;&#x4E86;&#x9019;&#x6A23;&#x7684;&#x63A5;&#x53E3;&#xFF1A;</p><p>https://gist.github.com/koromiko/9734097d9968a789b87aa9fcf7c27fb1</p><p>&#x9019;&#x500B;&#x63A5;&#x53E3;&#x8B93;ViewModel&#x80FD;&#x5920;&#x63A5;&#x6536;&#x4F7F;&#x7528;&#x8005;&#x7684;&#x884C;&#x70BA;&#xFF0C;&#x4E26;&#x4E14;&#x91DD;&#x5C0D;&#x9019;&#x6A23;&#x7684;&#x884C;&#x70BA;&#x505A;&#x51FA;&#x5C0D;&#x61C9;&#x7684;&#x52D5;&#x4F5C;&#x3002;&#x5C0D;ViewController&#x4F86;&#x8AAA;&#xFF0C;table view delegate&#x5C31;&#x53EF;&#x4EE5;&#x8B8A;&#x5F97;&#x66F4;&#x7C21;&#x55AE;&#x4E86;&#xFF1A;</p><p>https://gist.github.com/koromiko/df132d8c3b00df3e7a97ca26f5d498a7<br>View&#x4E00;&#x63A5;&#x6536;&#x5230;&#x4F7F;&#x7528;&#x8005;&#x7684;&#x52D5;&#x4F5C;&#xFF0C;&#x5C31;&#x99AC;&#x4E0A;&#x628A;&#x5B83;&#x50B3;&#x7D66;ViewModel&#xFF0C;&#x4E26;&#x4E14;&#x7531;ViewModel&#x900F;&#x904E;isAllowSegue&#x4F86;&#x6C7A;&#x5B9A;&#xFF0C;&#x5230;&#x5E95;&#x8A72;&#x4E0D;&#x8A72;&#x555F;&#x52D5;&#x9019;&#x500B;segue&#xFF0C;View&#x5C31;&#x662F;&#x4E00;&#x500B;&#x80FD;&#x5920;&#x9AD4;&#x5BDF;&#x4E0A;&#x610F;&#x7684;&#xFF0C;&#x6069;&#xFF0C;&#x597D;&#x5B98;&#x54E1;XD</p><h2 id="implementation-of-photolistviewmodel">Implementation of PhotoListViewModel</h2><p>&#x90A3;ViewModel&#x88E1;&#x9762;&#x662F;&#x9577;&#x600E;&#x6A23;&#x5462;&#xFF1F;&#x5728;&#x9019;&#x500B;&#x61C9;&#x7528;&#x4E2D;&#xFF0C;ViewModel&#x8CA0;&#x8CAC;&#x4E0A;&#x7DB2;&#x6293;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x8CC7;&#x6599;&#x8F49;&#x63DB;&#x6210;&#x4F9B;&#x5448;&#x73FE;&#x7684;PhotoListCellViewModel&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4F7F;&#x7528;&#x4E86;&#x4E00;&#x500B;array&#x4F86;&#x88DD;&#x9019;&#x9019;&#x4E9B;cellViewModels&#xFF1A;</p><p>https://gist.github.com/koromiko/c9dd601bfa9f98be35aaee7f1f57b78a<br>&#x5728;ViewModel&#x7684;&#x521D;&#x59CB;&#x5316;&#x968E;&#x6BB5;&#xFF0C;&#x6211;&#x5011;&#x505A;&#x5169;&#x4EF6;&#x4E8B;&#x60C5;&#xFF1A;</p><ol><li>&#x6CE8;&#x5165;dependency - APIService</li><li>&#x958B;&#x59CB;&#x6293;&#x53D6;&#x8CC7;&#x6599;</li></ol><p>https://gist.github.com/koromiko/85819122f0a76bd2d76115880e6bea24<br>&#x5728;&#x9019;&#x6BB5;code&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x6703;&#x8DDF;APIService&#x8981;&#x8CC7;&#x6599;&#xFF0C;&#x5728;&#x8981;&#x8CC7;&#x6599;&#x4E4B;&#x524D;&#x5148;&#x628A;isLoading&#x8A2D;&#x5B9A;&#x6210;true&#xFF0C;&#x56E0;&#x70BA;isLoading&#x6709;&#x8DDF;View&#x505A;&#x7D81;&#x5B9A;&#xFF0C;&#x6240;&#x4EE5;View&#x6703;&#x91DD;&#x5C0D;&#x9019;&#x500B;&#x4E8B;&#x4EF6;&#x8F49;&#x63DB;&#x6210;&#x8B80;&#x53D6;&#x4E2D;&#x7684;&#x6A23;&#x5F0F;&#x3002;&#x5728;&#x8CC7;&#x6599;&#x5168;&#x90E8;&#x90FD;&#x53D6;&#x4E0B;&#x4F86;&#x4E4B;&#x5F8C;&#xFF0C;&#x5728;processFetchedPhoto(photo:)&#x88E1;&#x9762;&#xFF0C;&#x628A;&#x8CC7;&#x6599;&#x8F49;&#x5316;&#x6210;&#x9069;&#x5408;&#x986F;&#x793A;&#x7684;&#x6A23;&#x5B50;(cellViewModels)&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x8B80;&#x53D6;&#x4E2D;&#x7684;&#x72C0;&#x614B;&#x8A2D;&#x5B9A;&#x6210;false&#xFF0C;&#x9019;&#x6642;&#x5019;View&#x6703;&#x56E0;&#x70BA;isLoading&#x8B8A;&#x6210;false&#xFF0C;activity indicator&#x5C31;&#x6703;&#x505C;&#x6B62;&#x8F49;&#x52D5;&#xFF0C;&#x4E5F;&#x6703;&#x56E0;&#x70BA;cellViewModel&#x7684;&#x66F4;&#x65B0;&#xFF0C;&#x91CD;&#x6574;table view&#x4E26;&#x4E14;&#x628A;&#x65B0;&#x7684;&#x8CC7;&#x6599;show&#x51FA;&#x4F86;&#x3002;</p><p>&#x4E0B;&#x9762;&#x662F;processFetchedPhoto&#x7684;&#x5BE6;&#x4F5C;&#xFF1A;</p><p>https://gist.github.com/koromiko/3078690354fb646d9eeae64c670503f5<br>&#x5B83;&#x628A;&#x6536;&#x5230;&#x7684;photo&#xFF0C;&#x900F;&#x904E;createCellViewModel( photo: Photo)&#x8F49;&#x6210;&#x4E86;&#x4E00;&#x500B;&#x4E00;&#x500B;&#x7684;CellViewModel&#xFF0C;&#x9019;&#x4E9B;CellViewModel&#xFF0C;&#x5728;&#x8CC7;&#x6599;&#x4E0A;&#x9577;&#x5F97;&#x8DDF;Cell&#x662F;&#x4E00;&#x6A23;&#x7684;&#xFF0C;&#x672A;&#x4F86;Cell&#x5728;&#x5448;&#x73FE;&#x6642;&#xFF0C;&#x5C31;&#x6703;&#x7167;&#x8457;CellViewModel&#x7684;&#x8CC7;&#x8A0A;&#xFF0C;&#x4E00;&#x4E94;&#x4E00;&#x5341;&#x5730;&#x53CD;&#x61C9;&#x51FA;&#x4F86;&#x3002;</p><p>Yay&#xFF01;&#x6211;&#x5011;&#x7D42;&#x65BC;&#x5B8C;&#x6210;&#x4E86;&#x6240;&#x6709;&#x7684;&#x7D81;&#x5B9A;&#x8DDF;&#x6539;&#x5BEB;&#xFF01;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/giphy-downsized-2.gif" class="kg-image" alt="giphy-downsized (2).gif" loading="lazy"></figure><p>&#x4EE5;&#x4E0A;&#x7684;&#x7A0B;&#x5F0F;&#xFF0C;&#x90FD;&#x53EF;&#x4EE5;&#x5728;&#x6211;&#x7684;Github&#x88E1;&#x9762;&#x627E;&#x5230;&#x3002;</p><p><a href="https://github.com/koromiko/Tutorial/tree/MVC/MVVMPlayground">Github - MVVMPlayground</a></p><p>&#x5176;&#x4E2D;MVC&#x7248;&#x7684;App&#x53EF;&#x4EE5;&#x7FFB;&#x5230;&#x201D;MVC&#x201D;&#x9019;&#x500B;tag&#xFF0C;&#x800C;&#x6700;&#x65B0;&#x7684;commit&#x5C31;&#x662F;MVVM&#x52A0;&#x4E0A;&#x6E2C;&#x8A66;&#x7684;&#x7248;&#x672C;&#x3002;</p><h2 id="%E2%80%9Cmvvm-is-not-very-good%E2%80%9D">&#x201C;MVVM is Not Very Good&#x201D;</h2><p>&#x5C31;&#x50CF;&#x4E0A;&#x9762;&#x63D0;&#x5230;&#x7684;&#xFF0C;&#x6C38;&#x9060;&#x6C92;&#x6709;&#x6700;&#x597D;&#x7684;&#x67B6;&#x69CB;&#xFF0C;&#x76F8;&#x4FE1;&#x4F60;&#x770B;&#x5B8C;&#x9019;&#x7BC7;&#x6587;&#x7AE0;&#x4E4B;&#x5F8C;&#xFF0C;&#x4E5F;&#x5927;&#x6982;&#x731C;&#x5F97;&#x5230;MVVM&#x6709;&#x751A;&#x9EBC;&#x554F;&#x984C;&#x4E86;&#xFF0C;&#x7DB2;&#x8DEF;&#x4E0A;&#x4E5F;&#x5DF2;&#x7D93;&#x6709;&#x883B;&#x591A;&#x95DC;&#x65BC;MVVM&#x7684;&#x7F3A;&#x9EDE;&#x7684;&#x8A0E;&#x8AD6;&#xFF0C;&#x5982;&#xFF1A;</p><p><a href="http://khanlou.com/2015/12/mvvm-is-not-very-good/">MVVM is Not Very Good - Soroush Khanlou</a></p><p><a href="http://www.danielhall.io/the-problems-with-mvvm-on-ios">The Problems with MVVM on iOS - Daniel Hall</a></p><p>&#x5176;&#x4E2D;&#xFF0C;&#x7B2C;&#x4E00;&#x7BC7;&#x53EF;&#x4EE5;&#x8AAA;&#x662F;&#x7832;&#x706B;&#x731B;&#x70C8;&#xFF0C;&#x4E26;&#x4E14;&#x5F15;&#x8D77;&#x4E86;&#x4E0D;&#x5C11;&#x8A0E;&#x8AD6;&#xFF0C;&#x5169;&#x7BC7;&#x4F5C;&#x8005;&#x5171;&#x540C;&#x7684;&#x8AD6;&#x9EDE;&#x662F;MVVM&#x5176;&#x5BE6;&#x8DDF;MVC&#x8DDF;&#x672C;&#x5DEE;&#x4E0D;&#x591A;&#xFF0C;&#x53EA;&#x662F;&#x628A;&#x4E00;&#x63A8;code&#x5F9E;viewController&#x79FB;&#x5230;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x5730;&#x65B9;&#xFF0C;&#x5B83;&#x5011;&#x9084;&#x662F;&#x4E00;&#x5806;code&#x3002;&#x9019;&#x500B;&#x8AAA;&#x6CD5;&#x57FA;&#x672C;&#x4E0A;&#x5FFD;&#x7565;&#x4E86;&#x5F88;&#x91CD;&#x8981;&#x7684;&#x4E00;&#x9EDE;&#xFF1A;&#x900F;&#x904E;MVVM&#xFF0C;&#x6211;&#x5011;&#x96E2;&#x53EF;&#x6E2C;&#x8A66;&#x7684;&#x7A0B;&#x5F0F;&#x78BC;&#x53C8;&#x8E8D;&#x9032;&#x4E86;&#x4E00;&#x5927;&#x6B65;&#x4E86;&#xFF01;&#x5C0D;ViewModel&#x4F86;&#x8AAA;&#xFF0C;&#x5B83;&#x5B8C;&#x5168;&#x6C92;&#x6709;View&#x7684;&#x5305;&#x88B1;&#xFF0C;&#x4F46;&#x53C8;&#x53EF;&#x4EE5;&#x5229;&#x7528;&#x7C21;&#x55AE;&#x7684;assert&#x4F86;&#x6E2C;&#x8A66;&#x5448;&#x73FE;&#x6548;&#x679C;&#x3002;MVVM&#x8DDF;&#x539F;&#x672C;&#x7684;MVC&#x4E4D;&#x770B;&#x4E4B;&#x4E0B;&#x5F88;&#x50CF;&#xFF0C;&#x4F46;&#x5C31;&#x6E2C;&#x8A66;&#x8DDF;&#x6B0A;&#x8CAC;&#x5206;&#x96E2;&#x4F86;&#x8AAA;&#xFF0C;&#x9084;&#x662F;&#x5F88;&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#x3002;</p><p>&#x5C0D;&#x65BC;&#x5C0F;&#x5F1F;&#x4F86;&#x8AAA;&#xFF0C;MVVM&#x6700;&#x5927;&#x7684;&#x7F3A;&#x9EDE;&#xFF0C;&#x5C31;&#x662F;controller&#x8DDF;presentation layer&#x7684;&#x5B9A;&#x7FA9;&#x6A21;&#x7CCA;&#xFF0C;&#x5927;&#x591A;&#x6578;(&#x5305;&#x542B;&#x6211;&#x81EA;&#x5DF1;)&#x7684;&#x4EBA;&#xFF0C;&#x90FD;&#x628A;controller&#x7684;&#x5DE5;&#x4F5C;&#xFF0C;&#x8DDF;presenter&#x4E00;&#x8D77;&#x653E;&#x5728;view model&#x88E1;&#x9762;&#x4E86;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;view model&#x540C;&#x6642;&#x53C8;&#x8CA0;&#x8CAC;&#x5354;&#x8ABF;&#x7DB2;&#x8DEF;&#x5C64;&#x3001;&#x8CC7;&#x6599;&#x5EAB;&#x5C64;(controller)&#xFF0C;&#x540C;&#x6642;&#x4E5F;&#x8655;&#x7406;&#x8F49;&#x63DB;&#x8CC7;&#x6599;&#x6210;&#x70BA;&#x53EF;&#x7D81;&#x5B9A;&#x7684;&#x5C0D;&#x8C61;(presenter)&#xFF0C;&#x4EE5;&#x6211;&#x5011;&#x7684;&#x76F8;&#x7247;app&#x4F86;&#x8AAA;&#xFF0C;PhotoListViewModel&#x505A;&#x4E86;&#x76F8;&#x7576;&#x591A;&#x7684;controller&#x4EFB;&#x52D9;&#xFF0C;&#x4F46;PhotoListCellViewModel&#x5C31;&#x662F;&#x500B;&#x55AE;&#x7D14;&#x7684;presenter&#x3002;</p><p>&#x53E6;&#x5916;&#xFF0C;MVVM&#x9084;&#x6709;&#x4E00;&#x500B;&#x975E;&#x5E38;&#x81F4;&#x547D;&#x7684;&#x7F3A;&#x9EDE;&#xFF0C;&#x5C31;&#x662F;&#x7F3A;&#x5C11;router&#x8DDF;builder&#x9019;&#x5169;&#x500B;&#x89D2;&#x8272;&#xFF0C;router&#x8CA0;&#x8CAC;&#x9801;&#x9762;&#x5207;&#x63DB;&#x7684;&#x908F;&#x8F2F;&#xFF0C;&#x800C;builder&#x8CA0;&#x8CAC;&#x521D;&#x59CB;&#x5316;&#x9019;&#x4E00;&#x5207;&#x3002;&#x9019;&#x5169;&#x500B;&#x89D2;&#x8272;&#xFF0C;&#x5728;&#x5927;&#x591A;&#x7684;MVVM&#x61C9;&#x7528;&#x4E2D;&#xFF0C;&#x90FD;&#x88AB;&#x5BEB;&#x5230;viewController&#x88E1;&#x9762;&#x4E86;&#x3002;</p><p>&#x4EE5;&#x4E0A;&#x9019;&#x5169;&#x9EDE;&#xFF0C;&#x7576;&#x7136;&#x4E5F;&#x5DF2;&#x7D93;&#x6709;&#x4EBA;&#x63D0;&#x51FA;&#x4E26;&#x4E14;&#x6709;&#x5C0D;&#x61C9;&#x7684;&#x89E3;&#x6CD5;&#x4E86;&#xFF0C;&#x5176;&#x4E2D;&#x4E00;&#x7A2E;&#x89E3;&#x6CD5;&#x662F;<a href="https://www.objc.io/issues/13-architecture/viper/">VIPER</a>&#x67B6;&#x69CB;&#xFF0C;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x5247;&#x662F;MVVM+FlowController(<a href="http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/">Improve your iOS Architecture with FlowControllers</a>)&#xFF0C;&#x9019;&#x5169;&#x500B;&#x90FD;&#x662F;&#x975E;&#x5E38;&#x68D2;&#x7684;&#x8A2D;&#x8A08;&#xFF0C;&#x5176;&#x4E2D;MVVM+FlowController&#x7684;&#x6982;&#x5FF5;&#x6211;&#x5F88;&#x559C;&#x6B61;&#xFF0C;&#x672A;&#x4F86;&#x6703;&#x518D;&#x91DD;&#x5C0D;router+builder&#x7684;&#x8B70;&#x984C;&#x7814;&#x7A76;&#x4E00;&#x4E0B;&#x518D;&#x505A;&#x5206;&#x4EAB;&#x3002;</p><h2 id="%E6%9E%B6%E6%A7%8B%E5%8F%AA%E6%98%AF%E8%BC%94%E5%8A%A9">&#x67B6;&#x69CB;&#x53EA;&#x662F;&#x8F14;&#x52A9;</h2><p>&#x5728;&#x958B;&#x767C;&#x4E16;&#x754C;&#x4E2D;&#xFF0C;&#x6C92;&#x6709;&#x6700;&#x597D;&#x7684;&#x67B6;&#x69CB;&#xFF0C;&#x8207;&#x5176;&#x722D;&#x8AD6;&#x90A3;&#x4E00;&#x500B;&#x6BD4;&#x8F03;&#x597D;&#x6216;&#x8005;&#x8AB0;&#x7528;&#x7684;&#x662F;&#x6B63;&#x7D71;&#x8AB0;&#x7528;&#x7684;&#x4E0D;&#x662F;&#xFF0C;&#x4E0D;&#x5982;&#x5148;&#x4E86;&#x89E3;&#x4E00;&#x4E0B;&#xFF0C;&#x9019;&#x4E9B;&#x67B6;&#x69CB;&#x51FA;&#x73FE;&#x7684;&#x524D;&#x56E0;&#x5F8C;&#x679C;&#xFF0C;&#x9084;&#x6709;&#x4ED6;&#x5011;&#x8EAB;&#x4E0A;&#x6240;&#x80CC;&#x8CA0;&#x7684;&#x4F7F;&#x547D;&#xFF0C;&#x5982;&#x679C;&#x80FD;&#x5920;&#x4E00;&#x76F4;&#x4FDD;&#x6301;&#x8457;&#x300C;&#x6211;&#x8981;&#x8B93;&#x6E2C;&#x8A66;&#x8B8A;&#x5F97;&#x597D;&#x5BEB;&#x300D;&#x9019;&#x6A23;&#x7684;&#x5FC3;&#x60C5;&#x53BB;&#x770B;&#x5F85;&#x9019;&#x4E9B;&#x67B6;&#x69CB;&#xFF0C;&#x5C31;&#x6703;&#x767C;&#x73FE;&#x4ED6;&#x5011;&#x5DF2;&#x7D93;&#x5F88;&#x6709;&#x6548;&#x5730;&#x5B8C;&#x6210;&#x4E86;&#x4ED6;&#x5011;&#x7684;&#x4EFB;&#x52D9;&#x4E86;&#x3002;&#x6211;&#x9078;&#x7528;MVVM&#x4F86;&#x67B6;&#x69CB;&#x6211;&#x7684;&#x7A0B;&#x5F0F;&#x7684;&#x7406;&#x7531;&#x975E;&#x5E38;&#x7C21;&#x55AE;&#xFF0C;&#x5C31;&#x662F;&#x5B83;&#x6BD4;&#x8F03;&#x597D;&#x7406;&#x89E3;&#xFF0C;&#x4E5F;&#x6BD4;&#x8F03;&#x5BB9;&#x6613;&#x4E0A;&#x624B;&#x3002;&#x53EF;&#x4EE5;&#x5EF6;&#x4F38;&#x95B1;&#x8B80;Soroush Khanlou&#x7684;&#x53E6;&#x5916;&#x4E00;&#x7BC7;&#x6587;&#x7AE0;<a href="http://khanlou.com/2014/09/8-patterns-to-help-you-destroy-massive-view-controller/">8 Patterns to Help You Destroy Massive View Controller</a>&#xFF0C;&#x88E1;&#x9762;&#x63D0;&#x5230;&#x4E86;&#x5F88;&#x591A;&#x67B6;&#x69CB;&#x7684;&#x57FA;&#x672C;&#x6CD5;&#x5247;&#xFF0C;&#x4F60;&#x6703;&#x767C;&#x73FE;&#x5927;&#x5BB6;&#x52AA;&#x529B;&#x7684;&#x65B9;&#x5411;&#x90FD;&#x662F;&#x985E;&#x4F3C;&#x7684;&#xFF0C;&#x90FD;&#x76E1;&#x91CF;&#x5E0C;&#x671B;&#x7269;&#x4EF6;&#x80FD;&#x5920;&#x6709;&#x55AE;&#x4E00;&#x7684;&#x8CAC;&#x4EFB;&#xFF0C;&#x90FD;&#x5E0C;&#x671B;&#x5229;&#x7528;composite pattern&#x4F86;decoupling&#x3002;&#x800C;&#x73FE;&#x968E;&#x6BB5;MVVM&#x7684;&#x8A2D;&#x8A08;&#x4E5F;&#x662F;&#x671D;&#x8457;&#x9019;&#x6A23;&#x7684;&#x65B9;&#x524D;&#x5728;&#x524D;&#x9032;&#x8457;&#x3002;</p><p>&#x4E0B;&#x4E00;&#x7BC7;&#x6211;&#x5011;&#x5C31;&#x8981;&#x4F86;&#x9032;&#x5165;&#x5E6B;MVVM&#x5BEB;&#x6E2C;&#x8A66;&#x7684;&#x4E16;&#x754C;&#x56C9;&#xFF01;&#x5C07;&#x4EE5;&#x4F11;&#x520A;&#x822C;&#x7684;&#x901F;&#x5EA6;&#x51FA;&#x520A;&#xFF0C;&#x656C;&#x8ACB;&#x671F;&#x5F85;&#xFF01;</p><p>&#x6587;&#x7AE0;&#x53C3;&#x8003;&#x975E;&#x5E38;&#x591A;&#x76F8;&#x95DC;&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x4F46;&#x9084;&#x662F;&#x5F88;&#x6015;&#x6709;&#x89C0;&#x5FF5;&#x4E0A;&#x7684;&#x8B2C;&#x8AA4;&#xFF0C;&#x5982;&#x679C;&#x6709;&#x8AA4;&#x6B61;&#x8FCE;&#x5927;&#x529B;&#x6307;&#x6B63;&#xFF0C;&#x4E5F;&#x6B61;&#x8FCE;&#x5728;&#x5E95;&#x4E0B;&#x52A0;&#x5165;&#x8A0E;&#x8AD6;&#x3002;</p><p>&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x76EE;&#x524D;&#x7E3D;&#x5171;&#x6709;&#x4E09;&#x96C6;&#xFF0C;&#x6B61;&#x8FCE;&#x4E00;&#x4F75;&#x89C0;&#x8CDE;&#xFF01;</p><p><a href="https://koromiko1104.wordpress.com/2017/07/30/unit-test-for-networking/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; Unit Test for Networking</a></p><p><a href="https://koromiko1104.wordpress.com/2017/10/05/unit-test-for-core-data/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; Unit Test for Core Data</a></p><p><a href="https://koromiko1104.wordpress.com/2017/10/05/mvvmapp/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; &#x539F;&#x4F86;&#x662F;&#x90A3;&#x500B;&#x50B3;&#x8AAA;&#x4E2D;&#x7684;MVVM&#x963F;</a></p><h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">&#x53C3;&#x8003;&#x8CC7;&#x6599;</h2><p><a href="https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/">Introduction to Model/View/ViewModel pattern for building WPF apps - John Gossman</a></p><p><a href="https://www.objc.io/issues/13-architecture/mvvm/">Introduction to MVVM - objc</a></p><p><a href="https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52">iOS Architecture Patterns - Bohdan Orlov</a></p><p><a href="http://swiftyjimmy.com/category/model-view-viewmodel/">Model-View-ViewModel with swift - SwiftyJimmy</a></p><p><a href="https://www.toptal.com/ios/swift-tutorial-introduction-to-mvvm">Swift Tutorial: An Introduction to the MVVM Design Pattern - &#xA0;DINO BARTO&#x160;AK</a></p><p><a href="https://msdn.microsoft.com/en-us/magazine/dn463790.aspx">MVVM - Writing a Testable Presentation Layer with MVVM - Brent Edwards</a></p><p><a href="http://rasic.info/bindings-generics-swift-and-mvvm/">Bindings, Generics, Swift and MVVM - Srdan Rasic</a></p><hr><p>&#x606D;&#x559C;&#x4F60;&#x5F88;&#x6709;&#x8010;&#x5FC3;&#x5730;&#x770B;&#x5230;&#x4E86;&#x9019;&#x88E1;(&#x76F4;&#x63A5;&#x6309;End&#x597D;&#x50CF;&#x4E5F;&#x6703;&#x5230;&#x9019;&#x908A;&#xFF1F;)&#xFF0C;&#x5EF6;&#x7E8C;&#x4E4B;&#x524D;&#x7684;&#x597D;&#x50B3;&#x7D71;(?)&#xFF0C;&#x5206;&#x4EAB;&#x4E00;&#x4E0B;&#x500B;&#x4EBA;&#x8FD1;&#x671F;&#x559C;&#x611B;&#x7684;&#x597D;&#x96FB;&#x5F71;&#xFF01;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/10/mv5bmdnhmdnhodqtoddhmi00m2vjlwjjm2etzgi0mdfiyzq3mtu5xkeyxkfqcgdeqxvyodawmtu1mte-_v1_sy1000_cr006781000_al_.jpg" class="kg-image" alt loading="lazy"></figure><p>&#x6700;&#x8FD1;&#x770B;&#x4E86;&#x4E00;&#x90E8;&#x96FB;&#x5F71;&#xFF0C;American History X&#xFF0C;&#x662F;1998&#x5E74;&#x7684;&#x96FB;&#x5F71;&#xFF0C;&#x4E3B;&#x89D2;&#x662F;&#x5F8C;&#x4F86;&#x6F14;&#x4E86;&#x8F5F;&#x52D5;&#x4E16;&#x754C;&#x7684;Fighting Club&#x7684;Edward Norton&#x3002;&#x6545;&#x4E8B;&#x662F;&#x5728;&#x63CF;&#x8FF0;&#x4E00;&#x500B;&#x5177;&#x6709;&#x7A2E;&#x65CF;&#x4E3B;&#x7FA9;&#x7684;&#x5929;&#x624D;&#x4E3B;&#x89D2;&#xFF0C;&#x5728;&#x5165;&#x7344;&#x524D;&#x5F8C;&#xFF0C;&#x8DDF;&#x5BB6;&#x4EBA;&#x9084;&#x6709;&#x793E;&#x5340;&#x7684;&#x4E92;&#x52D5;&#x8DDF;&#x5FC3;&#x8DEF;&#x6B77;&#x7A0B;&#x3002;&#x5C0E;&#x6F14;&#x5DE7;&#x5999;&#x5730;&#x5229;&#x7528;&#x4E00;&#x9577;&#x4E00;&#x77ED;&#x7684;&#x96D9;&#x6642;&#x9593;&#x8EF8;&#xFF0C;&#x6DF1;&#x523B;&#x5730;&#x63CF;&#x7E6A;&#x4E86;&#x54E5;&#x54E5;(3&#x5230;5&#x5E74;)&#x8DDF;&#x5F1F;&#x5F1F;(&#x4E00;&#x500B;&#x665A;&#x4E0A;)&#x7684;&#x4E92;&#x52D5;&#x8207;&#x6210;&#x9577;&#x3002;&#x9019;&#x90E8;&#x96FB;&#x5F71;&#x76F4;&#x63A5;&#x628A;&#x7F8E;&#x570B;&#x7684;&#x7A2E;&#x65CF;&#x4E3B;&#x7FA9;&#x554F;&#x984C;&#xFF0C;&#x6BEB;&#x4E0D;&#x63A9;&#x98FE;&#x5730;&#x642C;&#x4E0A;&#x6AAF;&#x9762;&#xFF0C;&#x903C;&#x89C0;&#x773E;&#x52A0;&#x5165;&#x9019;&#x500B;&#x6230;&#x5C40;&#xFF0C;&#x597D;&#x597D;&#x601D;&#x8003;&#x9019;&#x6A23;&#x53CD;&#x667A;&#x7684;&#x884C;&#x70BA;&#x662F;&#x600E;&#x6A23;&#x7522;&#x751F;&#x7684;&#xFF0C;&#x9084;&#x6709;&#x4ED6;&#x5011;&#x7684;&#x6B77;&#x53F2;&#x8108;&#x7D61;&#x3002;&#x559C;&#x6B61;&#x793E;&#x6703;&#x601D;&#x8003;&#x7684;&#x96FB;&#x5F71;&#x4EBA;&#x5343;&#x842C;&#x4E0D;&#x8981;&#x932F;&#x904E;&#xFF0C;&#x5C24;&#x5176;&#x9019;&#x6A23;&#x8D64;&#x88F8;&#x5730;&#x8B1B;&#x51FA;&#x7A2E;&#x65CF;&#x4E3B;&#x7FA9;&#x554F;&#x984C;&#x7684;&#x96FB;&#x5F71;&#x771F;&#x7684;&#x4E0D;&#x591A;&#xFF0C;&#x52A0;&#x4E0A;Edward Norton&#x975E;&#x5E38;&#x5B8C;&#x7F8E;&#x7684;&#x6F14;&#x51FA;&#xFF0C;&#x771F;&#x7684;&#x662F;&#x4E00;&#x90E8;&#x7D93;&#x5178;&#x597D;&#x96FB;&#x5F71;&#x3002;</p>]]></content:encoded></item><item><title><![CDATA[歡迎來到真實世界 - Unit Test for Networking]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/07/the-matrix143.jpg" class="kg-image" alt loading="lazy"></figure><p>&#x5728;&#x4E0A;&#x73ED;&#x901A;&#x52E4;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x6700;&#x5E38;&#x505A;&#x7684;&#x4E8B;&#x60C5;&#x5C31;&#x662F;&#x6253;&#x96FB;&#x52D5;&#x4E86;&#x3002;&#x5BE6;&#x5728;&#x6709;&#x9EDE;&#x96E3;&#x5728;&#x516C;&#x8ECA;&#x4E0A;&#x9762;&#x770B;&#x66F8;&#x6216;&#x505A;&#x8A8D;&#x771F;&#x7684;&#x4E8B;&#xFF0C;&#x8ACB;&#x554F;&#x9664;&#x4E86;&#x9AD8;&#x4E2D;&#x751F;&#x4E4B;&#x5916;</p>]]></description><link>https://huangshihting.works/blog/huan-ying-lai-dao-zhen-shi-shi-jie-unit-test-for-networking/</link><guid isPermaLink="false">61f2a01356cf0e0001441860</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Sat, 29 Jul 2017 15:00:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/07/the-matrix143.jpg" class="kg-image" alt loading="lazy"></figure><p>&#x5728;&#x4E0A;&#x73ED;&#x901A;&#x52E4;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x6700;&#x5E38;&#x505A;&#x7684;&#x4E8B;&#x60C5;&#x5C31;&#x662F;&#x6253;&#x96FB;&#x52D5;&#x4E86;&#x3002;&#x5BE6;&#x5728;&#x6709;&#x9EDE;&#x96E3;&#x5728;&#x516C;&#x8ECA;&#x4E0A;&#x9762;&#x770B;&#x66F8;&#x6216;&#x505A;&#x8A8D;&#x771F;&#x7684;&#x4E8B;&#xFF0C;&#x8ACB;&#x554F;&#x9664;&#x4E86;&#x9AD8;&#x4E2D;&#x751F;&#x4E4B;&#x5916;&#x8AB0;&#x6703;&#x5728;&#x516C;&#x8ECA;&#x4E0A;&#x8A8D;&#x771F;&#x505A;&#x4E8B;&#x7684;&#xFF1F;&#x90A3;&#x500B;&#x5948;&#x7C73;&#x77ED;&#x7684;&#x5C08;&#x6CE8;&#x6642;&#x9593;&#x8981;&#x601D;&#x8003;&#x4EFB;&#x4F55;&#x4E8B;&#x60C5;&#x90FD;&#x6709;&#x56F0;&#x96E3;&#x3002;&#x4F46;&#x662F;&#x6253;Clash Royal&#x5C31;&#x4E0D;&#x4E00;&#x6A23;&#x4E86;&#xFF0C;&#x90A3;&#x662F;&#x4E00;&#x6B3E;&#x6703;&#x8B93;&#x4F60;&#x5728;&#x6BEB;&#x79D2;&#x4E4B;&#x5167;&#x9032;&#x5165;&#x7CBE;&#x795E;&#x6642;&#x5149;&#x5C4B;&#x7684;&#x904A;&#x6232;&#xFF0C;&#x53EA;&#x8981;&#x958B;&#x6230;&#x4E4B;&#x5F8C;&#x4F60;&#x5C31;&#x6703;&#x5230;&#x77AC;&#x9593;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x4E16;&#x754C;&#xFF0C;&#x76F4;&#x5230;&#x6230;&#x9B25;&#x7D50;&#x675F;&#x524D;&#x4F60;&#x90FD;&#x4E0D;&#x6703;&#x56DE;&#x5230;&#x73FE;&#x5BE6;&#xFF0C;&#x5C31;&#x7B97;&#x5750;&#x904E;&#x7AD9;&#x4E5F;&#x662F;&#x4E00;&#x6A23;&#x3002;&#x96D6;&#x7136;&#x4E00;&#x5834;&#x53EA;&#x6709;&#x5169;&#x5206;&#x9418;&#xFF0C;&#x4F46;&#x5C0D;&#x4F60;&#x4F86;&#x8AAA;&#x6BCF;&#x4E00;&#x5834;&#x90FD;&#x50CF;&#x5217;&#x5BE7;&#x683C;&#x52D2;&#x570D;&#x57CE;&#x6230;&#x4E00;&#x6A23;&#xFF0C;&#x90FD;&#x662F;&#x6F2B;&#x9577;&#x7684;&#x6301;&#x4E45;&#x6230;&#xFF0C;&#x4E5F;&#x50CF;&#x53F0;&#x7063;&#x7684;&#x653F;&#x6CBB;&#x4E00;&#x6A23;&#xFF0C;&#x90FD;&#x662F;&#x5E73;&#x884C;&#x7684;&#x6642;&#x7A7A;&#x3002;</p><p>&#x5982;&#x679C;&#x60F3;&#x4E86;&#x5B8C;&#x6574;&#x7684;&#x5E73;&#x884C;&#x6642;&#x7A7A;&#xFF0C;&#x5C31;&#x4E0D;&#x80FD;&#x4E0D;&#x63D0;&#x99ED;&#x5BA2;&#x4EFB;&#x52D9;(the Matrix)&#xFF0C;&#x9019;&#x90E8;&#x96FB;&#x5F71;&#x4E00;&#x76F4;&#x90FD;&#x662F;&#x5FC3;&#x76EE;&#x4E2D;&#x6700;&#x7D93;&#x5178;&#x7684;&#x96FB;&#x5F71;&#x4E4B;&#x4E00;&#xFF0C;&#x9664;&#x4E86;&#x5B50;&#x5F48;&#x6642;&#x9593;&#x9019;&#x500B;&#x8B93;&#x4EBA;&#x842C;&#x5206;&#x9A5A;&#x8C54;&#x7684;&#x5617;&#x8A66;&#x4E4B;&#x5916;&#xFF0C;&#x8A31;&#x8A31;&#x591A;&#x591A;&#x7684;&#x54F2;&#x5B78;&#x554F;&#x984C;&#x5728;&#x9019;&#x90E8;&#x96FB;&#x5F71;&#x88E1;&#x9762;&#x90FD;&#x6709;&#x7C21;&#x55AE;&#x7684;&#x8457;&#x58A8;&#xFF0C;&#x662F;&#x4E00;&#x90E8;&#x65E2;&#x5927;&#x773E;&#x5316;&#x53C8;&#x6709;&#x9EDE;&#x71D2;&#x8166;&#x7684;&#x597D;&#x96FB;&#x5F71;&#xFF0C;&#x4E0B;&#x4E00;&#x90E8;&#x80FD;&#x5920;&#x76F8;&#x984C;&#x4E26;&#x8AD6;&#x7684;Cyberpunk&#x5546;&#x696D;&#x96FB;&#x5F71;&#xFF0C;&#x5927;&#x6982;&#x5C31;&#x53EA;&#x5269;Inception&#x4E86;&#x3002;</p><p>&#x6240;&#x4EE5;&#xFF0C;&#x5C0D;&#xFF0C;&#x5728;&#x91CD;&#x8981;&#x4F46;&#x7121;&#x7528;&#x7684;&#x524D;&#x60C5;&#x63D0;&#x8981;&#x4E4B;&#x5F8C;&#xFF0C;&#x5C31;&#x4F86;&#x9032;&#x5165;&#x5230;&#x6211;&#x5011;&#x4ECA;&#x5929;&#x7684;&#x4E3B;&#x984C;&#x4E86;&#xFF1A;&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#xFF01;&#x9019;&#x5C07;&#x6703;&#x662F;&#x4E00;&#x9023;&#x4E32;&#x7684;&#x5206;&#x4EAB;&#x6587;&#xFF0C;&#x60F3;&#x8981;&#x5206;&#x4EAB;&#x7684;&#x662F;&#x4E00;&#x500B;&#x5F88;&#x591A;&#x4EBA;(&#x5305;&#x62EC;&#x5C0F;&#x5F1F;&#x6211;)&#x90FD;&#x4E0D;&#x9858;&#x610F;&#x63D0;&#x5230;&#x7684;&#x201D;&#x90A3;&#x500B;&#x8B70;&#x984C;&#x201D;&#xFF1A;Unit Test in iOS&#x3002;&#x55EF;&#xFF0C;&#x5F88;&#x8F15;&#x6613;&#x5C31;&#x8B1B;&#x51FA;&#x4F86;&#x4E86;&#x3002;</p><h2 id="welcome-to-the-real-world">Welcome to the real world</h2><p>&#x4E00;&#x822C;&#x4F86;&#x8AAA;&#xFF0C;&#x5728;iOS&#x958B;&#x767C;&#x4E0A;&#xFF0C;&#x5BEB;&#x6E2C;&#x8A66;&#x9019;&#x4EF6;&#x4E8B;&#x4E0D;&#x7B97;&#x662F;&#x975E;&#x5E38;&#x7684;&#x666E;&#x904D;&#xFF0C;&#x6240;&#x4EE5;&#x76F8;&#x95DC;&#x7684;&#x8CC7;&#x6E90;&#x53EF;&#x80FD;&#x4E0D;&#x50CF;&#x5176;&#x5B83;&#x8A9E;&#x8A00;&#x9019;&#x9EBC;&#x591A;&#xFF0C;&#x5C0F;&#x5F1F;&#x5728;&#x9084;&#x5728;&#x73A9;&#x6C99;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x5C31;&#x70BA;&#x4E86;&#x5BEB;&#x6E2C;&#x8A66;&#x539F;&#x5730;&#x6253;&#x8F49;&#x4E86;&#x975E;&#x5E38;&#x4E45;&#xFF0C;&#x4E00;&#x76F4;&#x60F3;&#x8981;&#x4E86;&#x89E3;&#x67D0;&#x4E9B;&#x529F;&#x80FD;&#x8981;&#x600E;&#x6A23;&#x6E2C;&#x8A66;&#x3001;&#x8981;&#x600E;&#x6A23;&#x8A2D;&#x5B9A;&#x60C5;&#x5883;&#xFF0C;&#x4F46;&#x53C8;&#x6709;&#x9EDE;&#x4E0D;&#x77E5;&#x9053;&#x600E;&#x6A23;&#x4E0B;&#x624B;&#x3002;&#x6240;&#x4EE5;&#x9019;&#x4E00;&#x7CFB;&#x5217;&#x7684;&#x6587;&#x7AE0;&#x6703;&#x8A66;&#x8457;&#x628A;&#x5C0F;&#x5F1F;&#x770B;&#x904E;&#x7684;&#x8CC7;&#x6E90;&#x8DDF;&#x7406;&#x89E3;&#x7684;&#x5167;&#x5BB9;&#x3001;&#x9084;&#x6709;&#x5BE6;&#x52D9;&#x4E0A;&#x7684;&#x904B;&#x4F5C;&#x6574;&#x7406;&#x51FA;&#x4F86;&#xFF0C;&#x5E0C;&#x671B;&#x53EF;&#x4EE5;&#x62CB;&#x78DA;&#x5F15;&#x7389;&#xFF0C;&#x8B93;&#x5927;&#x5BB6;&#x90FD;&#x80FD;&#x5920;&#x8E0F;&#x5165;&#x9019;&#x500B;&#x6BBF;&#x5802;&#x4E4B;&#x4E2D;&#x3002;</p><p>&#x56E0;&#x70BA;&#x662F;&#x5206;&#x4EAB;&#x6587;&#xFF0C;&#x6240;&#x4EE5;&#x63A5;&#x4E0B;&#x4F86;&#x4E26;&#x4E0D;&#x6703;&#x5DE8;&#x7D30;&#x7030;&#x907A;&#x5730;&#x628A;&#x6240;&#x6709;&#x7D30;&#x7BC0;&#x8B1B;&#x51FA;&#x4F86;&#xFF0C;&#x4F46;&#x6211;&#x6C92;&#x63D0;&#x5230;&#x7684;&#x90FD;&#x6703;&#x662F;&#x7DB2;&#x8DEF;&#x4E0A;&#x5DF2;&#x7D93;&#x975E;&#x5E38;&#x5E38;&#x898B;&#x3001;&#x5DF2;&#x7D93;&#x6709;&#x6559;&#x5B78;&#x7684;&#xFF0C;&#x6240;&#x4EE5;&#x8ACB;&#x5927;&#x5BB6;&#x4E0D;&#x7528;&#x64D4;&#x5FC3;&#xFF0C;&#x53CD;&#x4E4B;&#x9019;&#x4E9B;&#x6587;&#x7AE0;&#x6703;&#x4EE5;&#x6574;&#x500B;&#x6E2C;&#x8A66;&#x7684;&#x8108;&#x7D61;&#x70BA;&#x4E3B;&#xFF0C;&#x5728;&#x89C0;&#x5FF5;&#x7684;&#x91D0;&#x6E05;&#x6703;&#x591A;&#x65BC;&#x8A9E;&#x6CD5;&#x7684;&#x63CF;&#x8FF0;&#x3002;</p><p>&#x7B2C;&#x4E00;&#x7BC7;&#x8981;&#x4F86;&#x8AC7;&#x7684;&#x5C31;&#x662F;&#xFF0C;&#x95DC;&#x65BC;&#x5BEB;&#x6E2C;&#x8A66;&#xFF0C;&#x8B93;&#x4EBA;&#x6700;&#x70BA;&#x56F0;&#x64FE;&#x3001;&#x4E5F;&#x662F;&#x6574;&#x500B;&#x6E2C;&#x8A66;101&#x4E2D;&#x4E00;&#x500B;&#x975E;&#x5E38;&#x91CD;&#x8981;&#x7684;&#x89C0;&#x5FF5;&#xFF1A;Depedency Injection&#x3002;</p><p>&#x5728;&#x5BEB;&#x55AE;&#x5143;&#x6E2C;&#x8A66;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x5982;&#x679C;&#x9047;&#x5230;&#x4F60;&#x8981;&#x6E2C;&#x8A66;&#x7684;&#x7269;&#x4EF6;&#xFF0C;&#x662F;&#x8DDF;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x6709;&#x4ECB;&#x63A5;&#x7684;&#xFF0C;&#x50CF;&#x662F;network&#x3001;database&#x7B49;&#x7B49;&#xFF0C;&#x5C31;&#x6703;&#x8B8A;&#x7684;&#x6BD4;&#x8F03;&#x4E0D;&#x5BB9;&#x6613;&#x6E2C;&#x8A66;&#xFF0C;&#x56E0;&#x70BA;&#x4F60;&#x7E3D;&#x4E0D;&#x6703;&#x5E0C;&#x671B;&#x6BCF;&#x6B21;&#x8DD1;&#x6E2C;&#x8A66;&#xFF0C;&#x90FD;&#x9700;&#x8981;&#x9023;&#x4E0A;&#x7DB2;&#x8DEF;&#xFF0C;&#x90FD;&#x9700;&#x8981;&#x63A5;&#x4E0A;&#x8CC7;&#x6599;&#x5EAB;&#x4E26;&#x4E14;&#x5BEB;&#x5165;&#x771F;&#x5BE6;&#x8CC7;&#x6599;&#x5427;&#x3002;&#x5BEB;&#x6E2C;&#x8A66;&#x6709;&#x500B;&#x5927;&#x539F;&#x5247;&#xFF1A;&#x4E0D;&#x8981;&#x4EF0;&#x8CF4;&#x4EFB;&#x4F55;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#xFF0C;&#x5982;&#x679C;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x9700;&#x8981;&#x4EF0;&#x8CF4;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#xFF0C;&#x90A3;&#x6211;&#x5011;&#x5C31;&#x6703;&#x9047;&#x5230;&#x8A31;&#x591A;&#x554F;&#x984C;&#xFF1A;</p><ol><li>&#x901F;&#x5EA6;&#x5F88;&#x6162;</li><li>&#x6E2C;&#x8A66;&#x8CC7;&#x6599;&#x6703;&#x6C59;&#x67D3;&#x5230;&#x771F;&#x5BE6;&#x74B0;&#x5883;</li><li>&#x4E00;&#x4F46;&#x63DB;&#x500B;&#x958B;&#x767C;&#x74B0;&#x5883;(&#x6C92;&#x7DB2;&#x8DEF;&#x7B49;&#x7B49;)&#x5C31;&#x7121;&#x6CD5;&#x958B;&#x767C;&#x4E86;</li></ol><p>&#x6240;&#x4EE5;&#x6700;&#x597D;&#x7684;&#x505A;&#x6CD5;&#xFF0C;&#x662F;&#x6211;&#x5011;&#x9700;&#x8981;&#x6709;&#x4E00;&#x500B;&#x865B;&#x64EC;&#x7684;&#x74B0;&#x5883;&#xFF0C;&#x8B93;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x90FD;&#x8DD1;&#x5728;&#x9019;&#x500B;&#x865B;&#x64EC;&#x7684;&#x74B0;&#x5883;&#x4E4B;&#x4E2D;&#xFF0C;&#x6240;&#x6709;&#x7684;&#x8ACB;&#x6C42;&#x90FD;&#x662F;&#x76F4;&#x63A5;&#x63A5;&#x5230;&#x9019;&#x500B;&#x5047;&#x74B0;&#x5883;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x3002;&#x9019;&#x6709;&#x9EDE;&#x50CF;&#x5728;the Matrix&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x53EA;&#x8981;&#x62BD;&#x63DB;&#x6389;&#x8166;&#x888B;&#x5F8C;&#x9762;&#x7684;&#x63D2;&#x982D;&#xFF0C;&#x5C31;&#x80FD;&#x5920;&#x628A;&#x74B0;&#x5883;&#x6CE8;&#x5165;&#x5230;&#x6211;&#x5011;&#x904B;&#x4F5C;&#x4E2D;&#x7684;&#x8166;&#x888B;&#x4E4B;&#x4E2D;&#xFF0C;&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x5011;&#x505A;&#x7684;&#x4E8B;&#x60C5;&#xFF0C;&#x90FD;&#x8DDF;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x4E16;&#x754C;&#x6C92;&#x6709;&#x95DC;&#x4FC2;&#xFF0C;&#x5728;&#x9019;&#x500B;&#x74B0;&#x5883;&#x4E4B;&#x4E2D;&#xFF0C;&#x56E0;&#x70BA;&#x5B83;&#x7684;&#x4E00;&#x5207;&#x7269;&#x7406;&#x7279;&#x6027;&#x90FD;&#x8DDF;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x4E00;&#x6A23;&#xFF0C;&#x5B78;&#x5230;&#x7684;&#x6771;&#x897F;&#x4E5F;&#x6703;&#x662F;&#x4E00;&#x6A23;&#x7684;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x5728;&#x5047;&#x74B0;&#x5883;&#x4E2D;&#x8A13;&#x7DF4;&#xFF0C;&#x56DE;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x6253;&#x9B25;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#xFF0C;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x5728;&#x6E2C;&#x8A66;&#x74B0;&#x5883;&#x4E2D;&#x628A;code&#x5BEB;&#x597D;&#xFF0C;&#x4E1F;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x4E2D;&#x4E5F;&#x80FD;&#x5920;&#x904B;&#x4F5C;&#x6B63;&#x5E38;&#x3002;</p><h2 id="tldr">TL;DR</h2><p>&#x4EE5;&#x4E0B;&#x5167;&#x5BB9;&#x6703;&#x63D0;&#x5230;&#xFF1A;</p><ol><li>&#x600E;&#x9EBC;&#x5229;&#x7528;Dependency Injection&#x8A2D;&#x8A08;&#x4E00;&#x500B;&#x66F4;&#x597D;&#x6E2C;&#x8A66;&#x7684;&#x7269;&#x4EF6;</li><li>&#x600E;&#x9EBC;&#x5229;&#x7528;Protocol&#x4F86;&#x88FD;&#x4F5C;mock&#x7269;&#x4EF6;</li><li>&#x600E;&#x9EBC;&#x6E2C;&#x8A66;&#x8CC7;&#x6599;&#x6B63;&#x78BA;&#x6027;&#x8DDF;&#x884C;&#x70BA;&#x6B63;&#x78BA;&#x6027;</li></ol><h2 id="dependency-injection-di">Dependency Injection (DI)</h2><p>Ok&#xFF0C;&#x56DE;&#x5230;&#x6211;&#x5011;&#x5373;&#x5C07;&#x8981;&#x64B0;&#x5BEB;&#x7684;&#x7A0B;&#x5F0F;&#x4E0A;&#xFF0C;&#x6211;&#x5011;&#x73FE;&#x5728;&#x8981;&#x5BE6;&#x505A;&#x4E00;&#x500B;&#xFF0C;&#x80FD;&#x5920;&#x767C;&#x51FA;http get&#x8ACB;&#x6C42;&#x7684;HttpClient&#x985E;&#x5225;&#xFF0C;&#x9019;&#x6A23;&#x7684;&#x985E;&#x5225;&#xFF0C;&#x5B83;&#x53EF;&#x80FD;&#x9700;&#x8981;&#x6EFF;&#x8DB3;&#x4EE5;&#x4E0B;&#x689D;&#x4EF6;&#xFF1A;</p><ol><li>&#x767C;&#x51FA;&#x7684;request&#x7684;URL&#x8981;&#x8DDF;&#x6211;&#x5011;&#x6307;&#x5B9A;&#x7684;&#x4E00;&#x6A23;</li><li>&#x8981;&#x771F;&#x7684;&#x6709;&#x767C;&#x51FA;request</li></ol><p>&#x597D;&#x7684;&#xFF0C;&#x6211;&#x5011;&#x5148;&#x54BB;&#x54BB;&#x54BB;&#x5730;&#x5BEB;&#x4E86;&#x4E00;&#x500B;HttpClient&#xFF1A;</p><pre><code>class HttpClient {

    typealias completeClosure = ( _ data: Data?, _ error: Error?)-&gt;Void

    func get( url: URL, callback: @escaping completeClosure ) {
        let request = NSMutableURLRequest(url: url)
        request.httpMethod = &quot;GET&quot;
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            callback(data, error)
        }
        task.resume()
        }

}
</code></pre><p>&#x770B;&#x8D77;&#x4F86;&#x9019;&#x500B;&#x7A0B;&#x5F0F;&#xFF0C;&#x53EF;&#x4EE5;&#x767C;&#x51FA;get request&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x8CC7;&#x6599;&#x900F;&#x904E;callback&#x9019;&#x500B;closure&#x56DE;&#x50B3;&#xFF0C;&#x6240;&#x4EE5;&#x5B83;&#x53EF;&#x4EE5;&#x9019;&#x6A23;&#x4F7F;&#x7528;&#xFF1A;</p><pre><code>HttpClient().get(url: url) { (success, response) in
    // Return data
}
</code></pre><p>&#x554F;&#x984C;&#x4F86;&#x4E86;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x600E;&#x6A23;&#x6E2C;&#x8A66;&#x5B83;&#x5462;&#xFF1F;&#x6211;&#x6BCF;&#x6B21;&#x53EA;&#x8981;&#x547C;&#x53EB;get(URL, completeClosure)&#x9019;&#x500B;method&#xFF0C;&#x5B83;&#x90FD;&#x6703;&#x76F4;&#x63A5;&#x6BEB;&#x7121;&#x61F8;&#x5FF5;&#x5730;&#x9023;&#x4E0A;&#x7DB2;&#xFF0C;&#x4E26;&#x4E14;&#x7FA9;&#x7121;&#x53CD;&#x9867;&#x5730;&#x4E0A;&#x6211;&#x6307;&#x5B9A;&#x7684;server&#x62FF;&#x8CC7;&#x6599;&#xFF0C;&#x9019;&#x6A23;&#x4E0D;&#x884C;&#xFF01;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4ED4;&#x7D30;&#x5730;&#x770B;&#x4E00;&#x4E0B;&#x9019;&#x652F;&#x7A0B;&#x5F0F;&#xFF0C;&#x4F60;&#x6703;&#x767C;&#x73FE;&#xFF0C;&#x76F4;&#x63A5;&#x9023;&#x4E0A;&#x7DB2;&#x8DEF;&#x7684;&#x95DC;&#x9375;&#xFF0C;&#x5728;&#x65BC;&#x90A3;&#x500B;&#x842C;&#x60E1;&#x7684;URLSession.shared&#x9019;&#x500B;singleton&#xFF0C;&#x53EA;&#x8981;&#x5B83;&#x4E00;&#x76F4;&#x5B58;&#x5728;&#x5728;&#x9019;&#x500B;&#x7A0B;&#x5F0F;&#x88E1;&#x9762;&#xFF0C;&#x6211;&#x5C31;&#x6BCF;&#x6B21;&#x90FD;&#x9700;&#x8981;&#x9023;&#x4E0A;&#x7DB2;&#x8DEF;&#xFF0C;&#x4E26;&#x4E14;&#x6293;&#x8CC7;&#x6599;&#x4E0B;&#x4F86;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5FC5;&#x9700;&#x8981;&#x4F86;&#x52D5;&#x624B;&#x6539;&#x9020;&#x5B83;&#xFF0C;URLSession&#x5C31;&#x662F;&#x4E00;&#x7A2E;&#x201D;&#x74B0;&#x5883;&#x201D;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x8B93;&#x5B83;&#x662F;&#x53EF;&#x4EE5;&#x66FF;&#x63DB;&#xFF0C;&#x53EF;&#x4EE5;&#x88AB;&#x201D;&#x6CE8;&#x5165;&#x201D;&#x7684;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x53C8;&#x54BB;&#x54BB;&#x54BB;&#x5730;&#x6539;&#x5BEB;&#x4E86;&#x9019;&#x500B;class&#xFF1A;</p><pre><code>class HttpClient {

    typealias completeClosure = ( _ data: Data?, _ error: Error?)-&gt;Void

    private let session: URLSession

    init(session: URLSessionProtocol) {
        self.session = session
    }
    
    func get( url: URL, callback: @escaping completeClosure ) {
        let request = NSMutableURLRequest(url: url)
        request.httpMethod = &quot;GET&quot;
        let task = session.dataTask(with: request) { (data, response, error) in
            callback(data, error)
        }
        task.resume()
    }

}
</code></pre><p>&#x6211;&#x5011;&#x628A;</p><pre><code>let task = URLSession.shared.dataTask()
</code></pre><p>&#x6539;&#x6210;&#x4E86;</p><pre><code>let task = session.dataTask()
</code></pre><p>&#x4E26;&#x4E14;&#x65B0;&#x589E;&#x4E86;&#x4E00;&#x500B;&#x8B8A;&#x6578;session&#xFF0C;&#x4E26;&#x4E14;&#x65B0;&#x589E;&#x4E86;&#x5C0D;&#x61C9;&#x7684;init&#x3002;&#x5F9E;&#x6B64;&#x4E4B;&#x5F8C;&#xFF0C;&#x6211;&#x5011;&#x5728;&#x5275;&#x5EFA;HttpClient&#x6642;&#xFF0C;&#x5C31;&#x9700;&#x8981;&#x6307;&#x5B9A;&#x9019;&#x500B;session&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#xFF0C;&#x6211;&#x5011;&#x5728;&#x5275;&#x5EFA;HttpClient&#x6642;&#xFF0C;&#x5C31;&#x9700;&#x8981;&#x628A;&#x5C0D;&#x61C9;&#x7684;&#x74B0;&#x5883;&#x201D;&#x6CE8;&#x5165;&#x201D;&#x9019;&#x500B;&#x7269;&#x4EF6;&#x4E4B;&#x4E2D;&#xFF0C;&#x5982;&#x679C;&#x6211;&#x5011;&#x653E;&#x4E86;&#x500B;&#x5047;session&#xFF0C;HttpClient&#x9084;&#x662F;&#x6703;&#x5728;&#x9019;&#x500B;&#x5047;session&#x4E4B;&#x4E2D;&#x6253;&#x6253;&#x6BBA;&#x6BBA;&#xFF0C;&#x4F46;&#x5C31;&#x5B8C;&#x5168;&#x4E0D;&#x6703;&#x78B0;&#x89F8;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x7684;URLSession.shared&#x4E86;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x7684;&#x61C9;&#x7528;&#x5C31;&#x8B8A;&#x6210;&#x4E86;&#xFF1A;</p><pre><code>HttpClient(session: SomeURLSession() ).get(url: url) { (success, response) in
    // Return data
}
</code></pre><p>&#x672A;&#x4F86;&#x5728;&#x4F7F;&#x7528;HttpClient&#x6642;&#xFF0C;&#x90FD;&#x9700;&#x8981;&#x6CE8;&#x610F;&#x9019;&#x908A;&#x6709;&#x500B;URLSession&#x7684;&#x76F8;&#x4F9D;&#x6027;&#xFF0C;&#x9700;&#x8981;&#x4F9D;&#x7167;&#x6211;&#x5011;&#x7684;&#x4F7F;&#x7528;&#x60C5;&#x5883;&#x4F86;&#x6CE8;&#x5165;&#x4E0D;&#x4E00;&#x6A23;&#x7684;session&#x3002;</p><p>&#x7576;&#x6211;&#x5011;&#x628A;&#x74B0;&#x5883;&#x62BD;&#x96E2;&#x4E4B;&#x5F8C;&#xFF0C;&#x8981;&#x5BEB;&#x6E2C;&#x8A66;&#x5C31;&#x8B8A;&#x5F97;&#x5F88;&#x5BB9;&#x6613;&#x4E86;&#xFF0C;&#x4F9D;&#x7167;&#x6211;&#x5011;&#x7684;&#x9700;&#x6C42;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x5BEB;&#x5169;&#x96BB;&#x7C21;&#x55AE;&#x7684;&#x55AE;&#x5143;&#x6E2C;&#x8A66;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x518D;&#x5EA6;&#x54BB;&#x54BB;&#x54BB;&#x5730;&#x5BEB;&#x51FA;&#x4E86;&#x4EE5;&#x4E0B;&#x7684;&#x6E2C;&#x8A66;&#x67B6;&#x69CB;&#xFF1A;</p><pre><code>class HttpClientTests: XCTestCase {
    var httpClient: HttpClient!
    let session = MockURLSession()

    override func setUp() {
        super.setUp()
        httpClient = HttpClient(session: session)
    }
    override func tearDown() {
        super.tearDown()
    }
}
</code></pre><p>&#x9019;&#x908A;&#x6211;&#x5011;&#x8A2D;&#x5B9A;&#x4E86;&#x4E00;&#x500B;session&#xFF0C;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x77E5;&#x9053;&#x6211;&#x5011;&#x7684;&#x7269;&#x4EF6;(HttpClient)&#x8DDF;&#x9019;&#x500B;&#x5047;&#x74B0;&#x5883;(session)&#x7684;&#x4E92;&#x52D5;&#x72C0;&#x6CC1;&#xFF0C;&#x9019;&#x500B;&#x5047;&#x74B0;&#x5883;&#x901A;&#x5E38;&#x88AB;&#x7A31;&#x505A;Mock&#xFF0C;&#x76EE;&#x5730;&#x5C31;&#x662F;&#x8981;&#x62FF;&#x4F86;&#x4E86;&#x89E3;&#x6211;&#x5011;&#x88FD;&#x4F5C;&#x4E2D;&#x7684;&#x7269;&#x4EF6;&#xFF0C;&#x662F;&#x4E0D;&#x662F;&#x6709;&#x4E56;&#x4E56;&#x5730;&#x57F7;&#x884C;&#x67D0;&#x4E9B;method&#xFF0C;&#x6216;&#x662F;&#x6709;&#x6C92;&#x6709;&#x505A;&#x67D0;&#x4E9B;&#x7279;&#x5B9A;&#x7684;&#x884C;&#x70BA;&#x3002;&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x53EF;&#x4EE5;&#x5728;setUp&#x88E1;&#x9762;&#x770B;&#x5230;&#xFF0C;&#x6211;&#x5011;&#x5275;&#x4E86;&#x4E00;&#x500B;HttpClient&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x9019;&#x500B;MockSession&#x6CE8;&#x5165;&#x4E86;&#x9019;&#x500B;HttpClient&#x4E4B;&#x4E2D;&#xFF0C;&#x6240;&#x4EE5;&#x73FE;&#x5728;&#xFF0C;&#x9019;&#x500B;HttpClient&#xFF0C;&#x5DF2;&#x7D93;&#x96E2;&#x958B;&#x6BCD;&#x9AD4;&#xFF0C;&#x5230;&#x4E86;&#x865B;&#x64EC;&#x7684;&#x4E16;&#x754C;&#x4E86;&#xFF01;&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x653E;&#x5FC3;&#x5730;&#x5BE6;&#x4F5C;&#x6211;&#x5011;&#x7684;&#x898F;&#x683C;&#xFF0C;&#x800C;&#x4E0D;&#x7528;&#x64D4;&#x5FC3;&#x901F;&#x5EA6;&#x8DDF;&#x6C59;&#x67D3;&#x8CC7;&#x6599;&#x7684;&#x554F;&#x984C;&#x4E86;&#x3002;</p><h2 id="test-data">Test data</h2><p>&#x597D;&#x7684;&#xFF0C;&#x73FE;&#x5728;&#x6211;&#x5011;&#x4F86;&#x770B;&#x770B;&#x6211;&#x5011;&#x7684;&#x7B2C;&#x4E00;&#x500B;&#x76EE;&#x6A19;&#xFF1A;</p><ol><li>&#x767C;&#x51FA;&#x7684;request&#x7684;URL&#x8981;&#x8DDF;&#x6211;&#x5011;&#x6307;&#x5B9A;&#x7684;&#x4E00;&#x6A23;</li></ol><p>&#x8EAB;&#x70BA;&#x4E00;&#x500B;&#x7A31;&#x8077;&#x7684;HttpClient&#xFF0C;&#x5C31;&#x9700;&#x8981;&#x80FD;&#x5920;&#x6B63;&#x78BA;&#x5730;&#x767C;&#x51FA;request&#xFF0C;&#x4E0D;&#x80FD;&#x4E82;&#x52D5;URL&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x54BB;&#x54BB;&#x54BB;&#x5730;&#x5BEB;&#x4E86;&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;get request&#x5230;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x4E4B;&#x4E2D;&#xFF1A;</p><pre><code>func test_get_request_with_URL() {

    guard let url = URL(string: &quot;https://mockurl&quot;) else {
        fatalError(&quot;URL can&apos;t be empty&quot;)
    }

    httpClient.get(url: url) { (success, response) in
        // Return data
    }

}
</code></pre><p>&#x63A5;&#x7E8C;&#x525B;&#x525B;&#x63D0;&#x5230;&#x7684;&#xFF0C;&#x60F3;&#x77E5;&#x9053;&#x6211;&#x5011;&#x7684;&#x5BE6;&#x4F5C;&#x662F;&#x4E0D;&#x662F;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x505A;&#x67D0;&#x4E9B;&#x52D5;&#x4F5C;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x5C0D;&#x6211;&#x5011;&#x7684;mock&#x7269;&#x4EF6;&#x52D5;&#x624B;&#x8173;&#xFF0C;&#x5C31;&#x4E0A;&#x9762;&#x9019;&#x5169;&#x500B;case&#x4F86;&#x8AAA;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x7684;&#x662F;&#xFF1A;</p><p>mock&#x7269;&#x4EF6;&#x8981;&#x6709;&#x500B;&#x63A5;&#x53E3;&#x8B93;&#x6211;&#x5011;&#x77E5;&#x9053;URLSession&#x6700;&#x5F8C;&#x767C;&#x51FA;&#x53BB;&#x7684;URL&#x662F;&#x751A;&#x9EBC;</p><p>&#x6240;&#x4EE5;&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x9032;&#x5165;&#x6211;&#x5011;&#x7684;&#x91CD;&#x982D;&#x6232;&#xFF1A;&#x600E;&#x6A23;&#x8A2D;&#x8A08;&#x9019;&#x500B;mock object&#x3002;&#x6211;&#x5011;&#x8981;&#x505A;&#x4E00;&#x500B;&#x9577;&#x5F97;&#x5F88;&#x50CF;URLSession&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8DDF;URLSession&#x6709;&#x4E00;&#x6A23;&#x7684;method&#x7684;&#x7269;&#x4EF6;&#xFF0C;&#x4E26;&#x4E14;&#x5728;&#x6211;&#x5011;mock&#x7684;URLSession&#x88E1;&#x9762;&#xFF0C;&#x57CB;&#x4E00;&#x4E9B;&#x80FD;&#x5920;&#x8A18;&#x9304;&#x7684;&#x8B8A;&#x6578;&#xFF0C;&#x597D;&#x8B93;&#x6211;&#x5011;&#x77E5;&#x9053;&#x6211;&#x5011;&#x7684;HttpClient&#x662F;&#x4E0D;&#x662F;&#x771F;&#x7684;&#x6709;call&#x90A3;&#x4E9B;method&#x3002;</p><p>&#x4E00;&#x822C;&#x6211;&#x5011;&#x8981;&#x767C;&#x51FA;&#x4E00;&#x500B;requesst&#xFF0C;&#x901A;&#x5E38;&#x6703;&#x9019;&#x6A23;&#x5BEB;&#xFF1A;</p><pre><code>let task = session.dataTask(with: request) { (data, response, error) in
    callback(data, error)
}
task.resume()
</code></pre><p>URLSession&#x7684;dataTask()&#x5C31;&#x662F;&#x6211;&#x5011;&#x60F3;mock&#x7684;&#x76EE;&#x6A19;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5148;&#x5BEB;&#x4E00;&#x500B;mock&#x67B6;&#x69CB;&#xFF1A;</p><pre><code>class MockURLSession {

    private (set) var lastURL: URL?

    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -&gt; URLSessionDataTask {

        lastURL = request.url

        completionHandler(nextData, successHttpURLResponse(request: request), nextError)        

        return // dataTask
    }

}
</code></pre><p>&#x4E0A;&#x9762;&#x9019;&#x5C31;&#x662F;&#x57FA;&#x672C;&#x7684;Mock&#x67B6;&#x69CB;&#xFF0C;&#x9019;&#x662F;&#x4E00;&#x500B;&#x4E92;&#x52D5;&#x8DDF;URLSession&#x4E00;&#x6A23;&#x7684;mock&#xFF0C;&#x80FD;&#x5920;&#x56DE;&#x50B3;dataTask&#xFF0C;&#x4E26;&#x4E14;&#x547C;&#x53EB;completionHandler&#x4F5C;&#x70BA;&#x6210;&#x529F;&#x7684;&#x56DE;&#x61C9;&#x3002;&#x5728;return&#x9019;&#x908A;&#x6211;&#x5011;&#x5148;&#x7559;&#x7A7A;&#x767D;&#xFF0C;&#x56E0;&#x70BA;dataTask&#x8DDF;Session&#x6709;&#x76F8;&#x4F9D;&#xFF0C;&#x9019;&#x662F;&#x53E6;&#x5916;&#x4E00;&#x500B;&#x6211;&#x5011;&#x9700;&#x8981;mock&#x7684;&#x6771;&#x897F;&#x3002;&#x56DE;&#x5230;&#x4E0A;&#x9762;&#x7684;code&#xFF0C;&#x6709;&#x4E00;&#x500B;&#x63A5;&#x53E3;&#x8DDF;URLSession&#x4E00;&#x6A21;&#x4E00;&#x6A23;&#x7684;datTask method&#xFF0C;&#x4F86;&#x8DDF;&#x6211;&#x5011;&#x7684;HttpClient&#x505A;&#x4E92;&#x52D5;&#x3002;&#x6211;&#x5011;&#x7684;&#x8A18;&#x9304;&#xFF0C;&#x5C31;&#x57CB;&#x5728;lastURL&#x9019;&#x500B;property&#x88E1;&#x9762;&#xFF0C;&#x6240;&#x4EE5;&#x4E00;&#x65E6;HttpClient&#x7684;get()&#x6E96;&#x5099;&#x767C;&#x51FA;request&#xFF0C;&#x5B83;&#x5C31;&#x6703;&#x547C;&#x53EB;&#x9019;&#x500B;datTask()&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x6700;&#x5F8C;&#x767C;&#x51FA;&#x53BB;&#x7684;URL&#x5B58;&#x5230;lastURL&#x9019;&#x500B;&#x8B8A;&#x6578;&#x88E1;&#x9762;&#x3002;</p><p>&#x53E6;&#x4E00;&#x65B9;&#x9762;&#xFF0C;&#x6211;&#x5011;&#x7684;test case&#x6703;&#x5BEB;&#x6210;&#x9019;&#x6A23;&#xFF1A;</p><pre><code>func test_get_request_with_URL() {

    guard let url = URL(string: &quot;https://mockurl&quot;) else {
        fatalError(&quot;URL can&apos;t be empty&quot;)
    }

    httpClient.get(url: url) { (success, response) in
        // Return data
    }

    XCTAssert(session.lastURL == url)
}
</code></pre><p>&#x53EA;&#x8981;assert lastURL&#x662F;&#x4E0D;&#x662F;&#x6709;&#x7B26;&#x5408;&#x6211;&#x5011;&#x8A2D;&#x5B9A;&#x7684;url&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x77E5;&#x9053;&#x6211;&#x5011;&#x767C;&#x51FA;&#x7684;get()&#x662F;&#x4E0D;&#x662F;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x8A2D;&#x5B9A;URL&#x4E86;&#x3002;</p><p>&#x5728;&#x4E0A;&#x9762;&#x7684;mock&#x5BE6;&#x4F5C;&#x4E2D;&#xFF0C;&#x6709;&#x4E00;&#x500B;&#x5730;&#x65B9;&#x6211;&#x5011;&#x4E26;&#x6C92;&#x6709;&#x5BEB;&#x5B8C;&#xFF0C;&#x5C31;&#x662F;&#x5728;<code>return // dataTask </code>&#x9019;&#x908A;&#xFF0C;&#x9019;&#x500B;&#x5730;&#x65B9;&#x7406;&#x8AD6;&#x4E0A;&#x8981;&#x56DE;&#x50B3;&#x4E00;&#x500B;URLSessionDataTask&#x7269;&#x4EF6;&#xFF0C;&#x4F46;&#x662F;&#x9019;&#x500B;&#x7269;&#x4EF6;&#x9700;&#x8981;&#x5F9E;&#x67D0;&#x500B;URLSession instance&#x5275;&#x5EFA;&#x51FA;&#x4F86;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x7684;mock URLSession&#x6C92;&#x6709;&#x9019;&#x500B;&#x529F;&#x80FD;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x9700;&#x8981;&#x518D;mock&#x4E00;&#x500B;URLSessionDataTask&#x3002;</p><pre><code>class MockURLSessionDataTask {   
    func resume() { }
}
</code></pre><p>&#x9019;&#x500B;mock&#xFF0C;&#x5C31;&#x53EA;&#x6709;&#x4E00;&#x500B;&#x529F;&#x80FD;&#xFF0C;&#x5C31;&#x662F;&#x4EFF;&#x88FD;dataTask&#x7684;resume()&#xFF0C;&#x9019;&#x6A23;&#x5728;&#x9032;&#x5230;&#x6211;&#x5011;&#x9019;&#x500B;&#x5047;&#x74B0;&#x5883;&#x4E4B;&#x5F8C;&#xFF0C;&#x5C31;&#x6703;&#x547C;&#x53EB;&#x9019;&#x500B;mock&#x7684;resume&#xFF0C;&#x4E4B;&#x5F8C;&#x5C31;&#x53EF;&#x4EE5;&#x5728;&#x9019;&#x908A;&#x505A;&#x8A18;&#x9304;&#xFF0C;&#x8A18;&#x9304;resume&#x662F;&#x4E0D;&#x662F;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x88AB;&#x547C;&#x53EB;&#xFF0C;&#x5F8C;&#x9762;&#x6703;&#x518D;&#x63D0;&#x5230;&#x3002;</p><p>&#x5230;&#x76EE;&#x524D;&#x70BA;&#x6B62;&#xFF0C;&#x9019;&#x4E9B;code&#x90FD;&#x662F;compile&#x4E0D;&#x904E;&#x7684;&#x3002;&#x4EBA;&#x751F;&#x5C31;&#x662F;&#x9019;&#x6A23;&#xFF0C;&#x4F60;&#x52AA;&#x529B;&#x4E86;&#x4E00;&#x5927;&#x5708;&#xFF0C;&#x537B;&#x767C;&#x73FE;&#x6700;&#x5F8C;&#x9084;&#x662F;compile&#x4E0D;&#x904E;&#x3002;&#x4F46;&#xFF0C;&#x5148;&#x5225;&#x5C0D;&#x4EBA;&#x751F;&#x5931;&#x671B;&#xFF01;&#x9019;&#x8DDF;&#x7576;&#x9B6F;&#x86C7;&#x4E0D;&#x4E00;&#x6A23;&#xFF0C;compile&#x5931;&#x6557;&#xFF0C;&#x53EA;&#x662F;&#x4E00;&#x6642;&#x7684;&#xFF0C;&#x662F;&#x53EF;&#x4EE5;&#x89E3;&#x6C7A;&#x7684;(&#x6709;&#x6C92;&#x6709;&#x5F88;&#x6B63;&#x5411;&#x601D;&#x8003;&#xFF01;)&#x3002;&#x5230;&#x76EE;&#x524D;&#x70BA;&#x6B62;&#xFF0C;compile&#x4E0D;&#x904E;&#xFF0C;&#x90FD;&#x662F;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x96D6;&#x7136;mock&#x4E86;&#x9019;&#x4E9B;&#x6771;&#x897F;&#xFF0C;&#x4F46;&#x5C0D;compiler&#x4F86;&#x8AAA;&#xFF0C;&#x9019;&#x4E9B;&#x6771;&#x897F;&#x4ECB;&#x9762;&#x4E0A;&#x9084;&#x4E0D;&#x80FD;&#x76F4;&#x63A5;&#x9019;&#x6A23;&#x7528;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x5229;&#x7528;protocol&#x4F86;&#x8B93;compiler&#x628A;&#x771F;&#x5BE6;&#x74B0;&#x5883;&#x8DDF;&#x6E2C;&#x8A66;&#x74B0;&#x5883;&#x90FD;&#x8996;&#x70BA;&#x4E00;&#x6A23;&#x7684;&#x3002;&#x56DE;&#x982D;&#x4F86;&#x770B;&#x4E00;&#x4E0B;&#x6211;&#x5011;&#x7684;HttpClient&#xFF1A;</p><pre><code> private let session: URLSession
</code></pre><p>&#x9019;&#x500B;private property&#x6211;&#x5011;&#x8A2D;&#x5B9A;&#x6210;URLSession&#xFF0C;&#x4F46;&#x662F;&#x6211;&#x5011;&#x88FD;&#x4F5C;&#x7684;mock&#x537B;&#x662F;MockURLSession&#xFF0C;&#x5169;&#x500B;&#x985E;&#x5225;&#x4E0D;&#x4E00;&#x6A23;&#xFF0C;compiler&#x6703;&#x5728;&#x547C;&#x53EB;&#x7684;&#x6642;&#x5019;&#x5831;&#x932F;&#xFF1A;</p><pre><code>class HttpClientTests: XCTestCase {
    var httpClient: HttpClient!
    let session = MockURLSession()

    override func setUp() {
        super.setUp()
        httpClient = HttpClient(session: session) // &#x9019;&#x908A;&#x6703;&#x70B8;
    }
    override func tearDown() {
        super.tearDown()
    }
}
</code></pre><p>&#x9019;&#x6642;&#x5019;&#xFF0C;&#x6211;&#x5011;&#x6709;&#x5E7E;&#x7A2E;&#x4F5C;&#x6CD5;&#x53EF;&#x4EE5;&#x9A19;&#x904E;compiler&#xFF0C;&#x4E00;&#x7A2E;&#x662F;&#x900F;&#x904E;subclass&#xFF0C;&#x8B93;MockURLSession&#x4E5F;&#x662F;URLSession&#x7684;subclass&#xFF0C;&#x5728;&#x9019;&#x908A;&#x6211;&#x5011;&#x4E0D;&#x4F7F;&#x7528;subclass&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x8981;mock&#x7684;&#x76EE;&#x6A19;&#x4E0D;&#x662F;&#x6211;&#x5011;&#x81EA;&#x5DF1;&#x7684;&#x7269;&#x4EF6;(URLSession)&#xFF0C;&#x5982;&#x679C;&#x7528;subclass&#xFF0C;&#x6709;&#x53EF;&#x80FD;&#x6703;&#x8AA4;&#x89F8;&#x5230;&#x6211;&#x5011;&#x6E2C;&#x8A66;&#x5B9A;&#x7FA9;&#x4EE5;&#x5916;&#x7684;&#x7BC4;&#x570D;&#x3002;</p><p>&#x53E6;&#x5916;&#x4E00;&#x7A2E;&#x65B9;&#x6CD5;&#xFF0C;&#x662F;&#x900F;&#x904E;protocol&#xFF0C;&#x8B93;URLSession&#x8DDF;MockURLSession&#x90FD;&#x9075;&#x8A62;&#x67D0;&#x7A2E;protocol&#xFF0C;&#x518D;&#x4FEE;&#x6539;</p><pre><code> private let session: URLSession
</code></pre><p>&#x6210;&#x70BA;</p><pre><code> private let session: URLSessionProtocol
</code></pre><p>&#x63A5;&#x4E0B;&#x4F86;&#x53EA;&#x8981;&#x8B93;URLSession&#x8DDF;MockURLSession&#x90FD;&#x7B26;&#x5408;&#x9019;&#x500B;&#x6211;&#x5011;&#x5B9A;&#x7FA9;&#x7684;URLSessionProtocol&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x9806;&#x5229;&#x5730;compile&#x4E86;&#x3002;&#x56E0;&#x70BA;HttpClient&#x7684;dependency&#x5F9E;&#x539F;&#x672C;&#x7684;URLSession&#x8B8A;&#x6210;URLSessionProtocol&#xFF0C;&#x6240;&#x4EE5;&#x4E4B;&#x5F8C;&#x6211;&#x5011;&#x7684;mock&#x4E0D;&#x7BA1;&#x600E;&#x6A23;&#x4FEE;&#x6539;&#xFF0C;&#x53EA;&#x8981;conform&#x9019;&#x500B;protocol&#xFF0C;&#x4F60;&#x5C31;&#x53EF;&#x4EE5;&#x6210;&#x70BA;&#x9019;&#x500B;HttpClient&#x7684;dependency&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#xFF0C;&#x53EA;&#x8981;&#x884C;&#x70BA;&#x6A21;&#x5F0F;&#x4E00;&#x6A23;&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x81EA;&#x7531;&#x66FF;&#x63DB;&#x9019;&#x500B;HttpClient&#x57F7;&#x884C;&#x7684;&#x74B0;&#x5883;&#x3002;</p><p>&#x9019;&#x500B;URLSessionProtocol&#xFF0C;&#x6211;&#x5011;&#x6703;&#x9019;&#x6A23;&#x8A2D;&#x8A08;&#xFF1A;</p><pre><code>protocol URLSessionProtocol {
    typealias DataTaskResult = (Data?, URLResponse?, Error?) -&gt; Void

    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -&gt; URLSessionDataTaskProtocol
}
</code></pre><blockquote>&#x70BA;&#x4E86;&#x65B9;&#x4FBF;&#x95B1;&#x8B80;&#xFF0C;&#x6211;&#x7FD2;&#x6163;&#x5E6B;closure&#x52A0;&#x4E0A;typeaslias&#x3002;</blockquote><p>&#x9019;&#x908A;&#x6211;&#x5011;&#x53EA;&#x5B9A;&#x7FA9;&#x4E86;&#x4E00;&#x500B;&#x9700;&#x8981;conform&#x7684;method&#xFF1A;<code>dataTask(NSURLRequest, DataTaskResult)</code>&#xFF0C;&#x56E0;&#x70BA;&#x76EE;&#x524D;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x5C31;&#x53EA;&#x6709;&#x9700;&#x8981;&#x9019;&#x500B;&#x3002;&#x672A;&#x4F86;&#x5982;&#x679C;&#x9700;&#x8981;&#x6E2C;&#x8A66;&#x66F4;&#x591A;&#x7684;&#x6771;&#x897F;&#xFF0C;&#x4F60;&#x5C31;&#x6703;&#x9700;&#x8981;&#x5728;&#x9019;&#x908A;&#x5B9A;&#x7FA9;&#x66F4;&#x591A;&#x7684;method&#xFF0C;&#x4F86;&#x8B93;test code&#x80FD;&#x5920;&#x53D6;&#x7528;&#x9019;&#x4E9B;method&#x3002;&#x9019;&#x500B;&#x6280;&#x5DE7;&#xFF0C;&#x5E38;&#x61C9;&#x7528;&#x5728;&#x7576;&#x6211;&#x5011;mock&#x4E0D;&#x5C6C;&#x65BC;&#x6211;&#x5011;&#x7684;&#x6771;&#x897F;(core data, network&#x7B49;&#x7B49;)&#x4E0A;&#x3002;</p><p>&#x9806;&#x9053;&#x4E00;&#x63D0;(&#x526F;&#x672C;&#x4E5F;&#x592A;&#x591A;)&#xFF0C;&#x8A31;&#x591A;&#x6E2C;&#x8A66;&#x7684;&#x539F;&#x5247;&#x90FD;&#x6709;&#x63D0;&#x5230;&#xFF0C;&#x6211;&#x5011;&#x4E0D;&#x80FD;mock&#x4E0D;&#x5C6C;&#x65BC;&#x6211;&#x5011;&#x7684;&#x6771;&#x897F;(don&#x2019;t mock things you don&#x2019;t own)&#xFF0C;&#x4F46;&#x70BA;&#x751A;&#x9EBC;&#x6211;&#x5011;&#x73FE;&#x5728;&#x537B;&#x7528;&#x4E86;&#x4E00;&#x5806;&#x7BC7;&#x5E45;(&#x52A0;&#x4E0A;&#x4E00;&#x5806;&#x5EE2;&#x8A71;)&#x5728;&#x8B1B;&#x5982;&#x4F55;mock&#x4E0D;&#x5C6C;&#x65BC;&#x6211;&#x5011;&#x7684;&#x6771;&#x897F;&#xFF1F;&#x5728;Test-Driven iOS Development with Swift 3&#x9019;&#x672C;&#x66F8;&#x88E1;&#x9762;&#x6709;&#x63D0;&#x5230;&#xFF0C;don&#x2019;t mock things you don&#x2019;t own&#x6307;&#x7684;&#x662F;&#xFF0C;&#x6211;&#x4E0D;&#x80FD;&#x53BB;mock third party&#x7684;&#x4EFB;&#x4F55;&#x6771;&#x897F;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x4E0D;&#x80FD;&#x78BA;&#x4FDD;&#x9019;&#x4E9B;&#x6771;&#x897F;&#xFF0C;&#x672A;&#x4F86;&#x5728;&#x66F4;&#x65B0;&#x7248;&#x672C;&#x4E4B;&#x5F8C;&#xFF0C;&#x662F;&#x4E0D;&#x662F;&#x6703;&#x898F;&#x683C;&#x6703;&#x4E00;&#x6A23;&#x3001;&#x884C;&#x70BA;&#x6A21;&#x5F0F;&#x4E5F;&#x4E00;&#x6A23;&#x3002;&#x8981;&#x662F;&#x898F;&#x683C;&#x6216;&#x884C;&#x70BA;&#x6A21;&#x5F0F;&#x6709;&#x8B8A;&#xFF0C;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x8F15;&#x5247;&#x4E0D;&#x904E;&#xFF0C;&#x91CD;&#x5247;&#x6703;&#x904E;&#x4E86;&#x4F46;&#x662F;&#x5176;&#x5BE6;&#x6709;&#x908F;&#x8F2F;&#x4E0A;&#x7684;&#x932F;&#x8AA4;&#xFF0C;&#x9019;&#x662F;&#x7CFB;&#x7D71;&#x8A2D;&#x8A08;&#x4E0A;&#x7684;&#x96F7;&#x5340;&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4E0D;mock&#x9019;&#x4E9B;&#x6211;&#x5011;&#x4E0D;&#x80FD;&#x63A7;&#x5236;&#x7684;&#x6771;&#x897F;&#xFF0C;&#x4F46;first-party&#x7684;&#x6771;&#x897F;&#xFF0C;&#x76F8;&#x5C0D;&#x7A69;&#x5B9A;&#xFF0C;&#x4E26;&#x4E14;&#x662F;&#x6240;&#x6709;&#x4EBA;&#x90FD;&#x6709;&#x5171;&#x8B58;&#x7684;&#xFF0C;&#x9019;&#x4E9B;&#x662F;&#x4F60;&#x53EF;&#x4EE5;&#x53BB;mock&#x7684;&#x3002;</p><p>&#x56DE;&#x5230;&#x525B;&#x525B;&#x7684;protocol&#xFF0C;&#x9084;&#x8A18;&#x5F97;&#x539F;&#x672C;&#x6A19;&#x6E96;&#x7684;URLSession&#x88E1;&#x9762;&#xFF0C;dataTask()&#x56DE;&#x50B3;&#x7684;&#x6771;&#x897F;&#x55CE;&#xFF1F;&#x539F;&#x672C;&#x56DE;&#x50B3;&#x7684;&#x662F;&#x4E00;&#x500B;URLSessionDataTask&#xFF0C;&#x9019;&#x53C8;&#x662F;&#x4E00;&#x500B;&#x4E0D;&#x5C6C;&#x65BC;&#x6211;&#x5011;&#x7684;method&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x4E5F;&#x8981;&#x52D5;&#x624B;mock&#x5B83;&#xFF01;&#x662F;&#x4E0D;&#x662F;&#x4F60;&#x7684;&#x96D9;&#x624B;&#x5DF2;&#x7D93;&#x958B;&#x59CB;&#x4E0D;&#x7531;&#x81EA;&#x4E3B;&#x5730;&#x6253;&#x5B57;&#x4E86;&#xFF1F;&#x6C92;&#x932F;&#xFF0C;&#x5C31;&#x662F;&#x4F60;&#x60F3;&#x7684;&#x90A3;&#x6A23;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x4E00;&#x500B;URLSessionDataTaskProtocol&#xFF01;</p><pre><code>protocol URLSessionDataTaskProtocol {
    func resume()
}
</code></pre><p>&#x9019;&#x500B;protocol&#x66F4;&#x7C21;&#x55AE;&#xFF0C;&#x56E0;&#x70BA;&#x6211;&#x5011;&#x53EA;&#x6703;&#x7528;&#x5230;resume()&#xFF0C;&#x6240;&#x4EE5;&#x5148;&#x5B9A;&#x7FA9;&#x5B83;&#x3002;</p><p>&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x9084;&#x6709;&#x4E00;&#x500B;&#x554F;&#x984C;&#x8981;&#x89E3;&#x6C7A;&#xFF0C;&#x525B;&#x525B;&#x90A3;&#x5169;&#x500B;protocol&#xFF0C;&#x6211;&#x5011;&#x90FD;&#x76F4;&#x63A5;&#x5957;&#x5230;&#x6211;&#x5011;&#x7684;MockURLSession&#x8DDF;MockURLSessionDataTask&#x4E0A;&#xFF0C;&#x4E26;&#x4E14;&#x6211;&#x5011;&#x4E5F;&#x90FD;&#x4E56;&#x4E56;&#x5730;&#x5BE6;&#x4F5C;protocol&#x6240;&#x9700;&#x8981;&#x7684;method&#x4E86;&#xFF0C;&#x73FE;&#x5728;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x771F;&#x5BE6;&#x7684;URLSession&#x8DDF;URLSessionDataTask&#x4E5F;&#x7B26;&#x5408;&#x9019;&#x5169;&#x500B;protocol&#xFF1A;</p><pre><code>extension URLSession: URLSessionProtocol {}

extension URLSessionDataTask: URLSessionDataTaskProtocol {}
</code></pre><p>URLSessionDataTask&#x6C92;&#x751A;&#x9EBC;&#x5927;&#x554F;&#x984C;&#xFF0C;&#x5B83;&#x672C;&#x4F86;&#x5C31;&#x6709;resume&#xFF0C;&#x4E26;&#x4E14;&#x9577;&#x5F97;&#x4E00;&#x6A21;&#x4E00;&#x6A23;&#xFF0C;&#x6240;&#x4EE5;&#x76F4;&#x63A5;&#x5957;&#x4E0A;&#x9019;&#x500B;protocol&#x662F;ok&#x7684;&#xFF0C;&#x4F46;&#x662F;&#x5C31;URLSession&#x4F86;&#x8AAA;&#xFF0C;&#x539F;&#x672C;&#x7684;dataTask()&#x56DE;&#x50B3;&#x7684;&#x662F;URLSessionDataTask&#xFF0C;&#x4F46;&#x6211;&#x5011;&#x65B0;&#x7684;dataTask()&#x56DE;&#x50B3;&#x7684;&#x537B;&#x5FC5;&#x9808;&#x662F;URLSessionDataTaskProtocol&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5982;&#x679C;&#x76F4;&#x63A5;&#x5957;&#x4E0A;URLSessionProtocol&#xFF0C;&#x6211;&#x5011;&#x9084;&#x662F;&#x7121;&#x6CD5;conform&#x9019;&#x500B;protocol&#xFF0C;&#x56E0;&#x70BA;&#x8DDF;&#x672C;&#x5C31;&#x4E0D;&#x5B58;&#x5728;&#x9019;&#x500B;method&#x3002;We need to do something!</p><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x65B0;&#x589E;&#x4E86;&#x4E00;&#x500B;method&#xFF1A;</p><pre><code>extension URLSession: URLSessionProtocol {
    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -&gt; URLSessionDataTaskProtocol {
        return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTaskProtocol
    }
}
</code></pre><p>&#x9019;&#x500B;func&#x505A;&#x7684;&#x4E8B;&#x60C5;&#xFF0C;&#x55AE;&#x7D14;&#x5C31;&#x662F;&#x539F;&#x672C;dataTask&#x7684;&#x63A5;&#x53E3;&#xFF0C;&#x5F9E;&#x539F;&#x672C;&#x56DE;&#x50B3;URLSessionDataTask&#xFF0C;&#x6539;&#x6210;&#x56DE;&#x50B3;URLSessionDataTaskProtocol&#xFF0C;&#x53EA;&#x6709;&#x5B9A;&#x7FA9;&#x4E0A;&#x6709;&#x8B8A;&#x5316;&#xFF0C;&#x672C;&#x8CEA;&#x4E0A;&#x662F;&#x5B8C;&#x5168;&#x4E0D;&#x8B8A;&#x7684;&#xFF0C;&#x95DC;&#x9375;&#x5C31;&#x5728;&#x65BC;&#x5229;&#x7528;as&#x5C07;&#x505A;&#x7C21;&#x55AE;&#x578B;&#x5225;&#x8F49;&#x63DB;&#xFF0C;&#x4F46;&#x5B8C;&#x5168;&#x4E0D;&#x5F71;&#x97FF;&#x7269;&#x4EF6;&#x672C;&#x8EAB;&#xFF0C;&#x53EA;&#x662F;&#x70BA;&#x4E86;conform&#x9019;&#x500B;protocol&#x800C;&#x5B58;&#x5728;&#x3002;</p><p>&#x6700;&#x5F8C;&#xFF0C;&#x518D;&#x56DE;&#x5230;&#x525B;&#x525B;&#x7684;MockURLSession&#x88E1;&#xFF1A;</p><pre><code>class MockURLSession {

    private (set) var lastURL: URL?

    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -&gt; URLSessionDataTask {

        lastURL = request.url

        completionHandler(nextData, successHttpURLResponse(request: request), nextError)        

        return // dataTask
    }

}
</code></pre><p>&#x90A3;&#x500B;<code>return // dataTask</code>&#xFF0C;&#x662F;&#x6642;&#x5019;&#x7D66;&#x5B83;&#x540D;&#x4EFD;(?)&#x4E86;&#xFF1A;</p><pre><code>class MockURLSession: URLSessionProtocol {

    var nextDataTask = MockURLSessionDataTask()

    private (set) var lastURL: URL?

    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -&gt; URLSessionDataTaskProtocol {
    lastURL = request.url

        completionHandler(nextData, successHttpURLResponse(request: request), nextError)
        return nextDataTask
    }

}
</code></pre><p>&#x6240;&#x4EE5;&#x9019;&#x500B;MockURLSession&#x7684;dataTask&#x56DE;&#x50B3;&#x7684;&#xFF0C;&#x5C31;&#x662F;&#x4E00;&#x500B;MockURLSessionDataTask()&#xFF0C;&#x4E26;&#x4E14;&#x5B83;&#x662F;&#x53EF;&#x4EE5;&#x57F7;&#x884C;resume()&#x7684;&#x3002;&#x9019;&#x6642;&#x5019;&#xFF0C;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;&#x5C31;&#x53EF;&#x4EE5;&#x88AB;&#x57F7;&#x884C;&#x4E26;&#x4E14;&#x5E25;&#x5E25;&#x5730;&#x901A;&#x904E;&#x4E86;&#xFF01;</p><p>&#x597D;&#x4E86;&#xFF0C;&#x7B2C;&#x4E00;&#x500B;&#x6E2C;&#x8A66;&#x5230;&#x76EE;&#x524D;&#x70BA;&#x6B62;&#x5DF2;&#x7D93;&#x6B63;&#x5F0F;&#x5B8C;&#x7562;&#xFF0C;&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x5011;&#x8981;&#x4F86;&#x770B;&#x7B2C;&#x4E8C;&#x500B;&#x6E2C;&#x8A66;&#x4E86;&#xFF01;</p><h2 id="test-behavior">Test Behavior</h2><p>&#x6211;&#x5011;&#x7684;&#x7B2C;&#x4E8C;&#x500B;&#x6E2C;&#x8A66;&#x689D;&#x4EF6;&#x662F;&#xFF1A;</p><p><code>&#x8981;&#x771F;&#x7684;&#x6709;&#x767C;&#x51FA;request</code></p><p>&#x6C92;&#x932F;&#xFF0C;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x6211;&#x5011;&#x7684;&#x5B50;&#x5F1F;&#x5175;&#x5011;&#x90FD;&#x8981;&#x4E56;&#x4E56;&#x505A;&#x4E8B;&#xFF0C;&#x4E0D;&#x8981;&#x6C92;&#x505A;&#x4F46;&#x537B;&#x8DDF;&#x6211;&#x8AAA;&#x505A;&#x4E86;&#x3002;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x9019;&#x4E9B;&#x5B50;&#x5F1F;&#x5175;&#x5728;&#x6AA2;&#x67E5;&#x85E5;&#x5BA4;&#x6709;&#x7121;&#x5B50;&#x5F48;&#x6642;&#xFF0C;&#x90FD;&#x8981;&#x5927;&#x8072;&#x5730;&#x558A;&#x51FA;&#x201D;&#x7121;&#x201D;&#xFF0C;&#x8868;&#x793A;&#x4ED6;&#x5011;&#x771F;&#x7684;&#x6709;&#x6AA2;&#x67E5;&#xFF08;&#x4F8B;&#x5B50;&#x602A;&#x602A;&#x7684;&#xFF09;(&#x6709;&#x4EBA;&#x771F;&#x7684;&#x6703;&#x6AA2;&#x67E5;&#xFF1F;&#xFF09;&#xFF08;&#x85E5;&#x5BA4;&#x5728;&#x90A3;&#x88E1;&#xFF1F;&#xFF09;</p><p>&#x597D;&#x7684;&#xFF0C;&#x73FE;&#x5728;&#x9019;&#x500B;&#x6E2C;&#x8A66;&#x8DDF;&#x525B;&#x525B;&#x4E0D;&#x4E00;&#x6A23;&#xFF0C;&#x525B;&#x525B;lastURL&#x8981;&#x6E2C;&#x7684;&#x662F;&#x8CC7;&#x6599;&#x662F;&#x4E0D;&#x662F;&#x6B63;&#x78BA;&#xFF0C;&#x800C;&#x9019;&#x500B;&#x6E2C;&#x8A66;&#x9700;&#x8981;&#x77E5;&#x9053;&#x7684;&#x662F;&#xFF0C;&#x67D0;&#x500B;method&#x6709;&#x6C92;&#x6709;&#x771F;&#x7684;&#x88AB;&#x547C;&#x53EB;&#x5230;&#x3002;&#x6211;&#x5011;&#x60F3;&#x77E5;&#x9053;request&#x6709;&#x6C92;&#x6709;&#x771F;&#x7684;&#x88AB;&#x767C;&#x51FA;&#x53BB;&#xFF0C;&#x7528;&#x5B85;&#x5B85;&#x7684;&#x8A71;&#x4F86;&#x8AAA;&#xFF0C;&#x5C31;&#x662F;dataTask().resume()&#x6709;&#x6C92;&#x6709;&#x771F;&#x7684;&#x88AB;&#x547C;&#x53EB;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#xFF0C;&#x6211;&#x5011;&#x53EA;&#x8981;&#x5728;&#x6211;&#x5011;&#x7684;&#x5047;&#x74B0;&#x5883;&#x7684;resume&#x88E1;&#x9762;&#xFF0C;&#x505A;&#x500B;&#x7C21;&#x55AE;&#x7684;&#x8A18;&#x9304;&#xFF0C;&#x5C31;&#x53EF;&#x4EE5;&#x77E5;&#x9053;&#x5B83;&#x6709;&#x6C92;&#x6709;&#x88AB;call&#x904E;&#x4E86;&#x3002;</p><p>&#x6211;&#x5011;&#x5148;&#x5BEB;&#x597D;&#x6211;&#x5011;&#x7684;&#x6E2C;&#x8A66;code&#xFF1A;</p><pre><code>func test_get_resume_called() {

    let dataTask = MockURLSessionDataTask()
    session.nextDataTask = dataTask

    guard let url = URL(string: &quot;https://mockurl&quot;) else {
        fatalError(&quot;URL can&apos;t be empty&quot;)
    }

    httpClient.get(url: url) { (success, response) in
        // Return data
    }

    XCTAssert(dataTask.resumeWasCalled)
}
</code></pre><p>resumeWasCalled&#x5C31;&#x662F;&#x6211;&#x5011;&#x60F3;&#x6E2C;&#x8A66;&#x7684;&#x76EE;&#x6A19;&#xFF0C;&#x5982;&#x679C;&#x5B83;&#x662F;true&#xFF0C;&#x5C31;&#x8868;&#x793A;resume()&#x771F;&#x7684;&#x6709;&#x88AB;&#x57F7;&#x884C;&#xFF0C;&#x56E0;&#x70BA;&#x57F7;&#x884C;resume&#x7684;&#x662F;URLSessionDataTask&#xFF0C;&#x6240;&#x4EE5;&#x6211;&#x5011;&#x628A;resumeWasCalled&#x8A2D;&#x8A08;&#x5728;dataTask&#x88E1;&#x9762;&#xFF0C;&#x800C;&#x9019;&#x500B;dataTask&#xFF0C;&#x5F88;&#x525B;&#x597D;&#xFF0C;&#x4E5F;&#x662F;&#x6211;&#x5011;&#x7684;&#x4EBA;&#xFF01;</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/07/5j6gzzxmlxkhdpkc68cu2l.jpg" class="kg-image" alt loading="lazy"></figure><p>&#x6240;&#x4EE5;&#x6211;&#x5011;&#x53EF;&#x4EE5;&#x8F15;&#x6613;&#x5730;&#x5728;&#x88E1;&#x9762;&#x591A;&#x52A0;&#x4E00;&#x500B;property&#xFF1A;</p><pre><code>class MockURLSessionDataTask: URLSessionDataTaskProtocol {
    private (set) var resumeWasCalled = false

    func resume() {
        resumeWasCalled = true
    }
}
</code></pre><p>&#x53EA;&#x8981;&#x6709;&#x4EBA;&#x5728;&#x9019;&#x500B;Mock&#x74B0;&#x5883;&#x88E1;&#x547C;&#x53EB;resume()&#xFF0C;resumeWasCalled&#x5C31;&#x6703;&#x8B8A;&#x6210;true&#xFF0C;&#x6211;&#x5011;&#x5C31;&#x53EF;&#x4EE5;&#x5728;&#x6E2C;&#x8A66;code&#x88E1;&#x9762;&#x5224;&#x65B7;resume&#x662F;&#x4E0D;&#x662F;&#x6709;&#x88AB;&#x547C;&#x53EB;&#x4E86;&#xFF01;</p><p>&#x662F;&#x4E0D;&#x662F;&#x5F88;&#x7C21;&#x55AE;&#x963F;&#xFF01;(&#x4E0D;&#x662F;)</p><h2 id="recap">Recap</h2><p>&#x5728;&#x4E0A;&#x9762;&#x7684;&#x6587;&#x7AE0;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x4E86;&#x89E3;&#x5230;&#x4E86;&#xFF1A;</p><ol><li>&#x600E;&#x9EBC;&#x5229;&#x7528;DI&#x4F86;&#x62BD;&#x63DB;&#x74B0;&#x5883;</li><li>&#x5229;&#x7528;protocol&#x4F86;&#x78BA;&#x4FDD;&#x5404;&#x7A2E;&#x74B0;&#x5883;&#x7684;&#x63A5;&#x53E3;&#x662F;&#x4E00;&#x81F4;&#x7684;</li><li>&#x600E;&#x6A23;&#x505A;&#x8CC7;&#x6599;&#x6B63;&#x78BA;&#x6027;&#x7684;&#x55AE;&#x5143;&#x6E2C;&#x8A66;</li><li>&#x600E;&#x6A23;&#x505A;&#x884C;&#x70BA;&#x7684;&#x55AE;&#x5143;&#x6E2C;&#x8A66;</li></ol><p>&#x6240;&#x6709;&#x7684;&#x7A0B;&#x5F0F;&#x90FD;&#x64FA;&#x5728;<a href="https://github.com/koromiko/Tutorial/blob/master/NetworkingUnitTest.playground/Contents.swift">Github</a>&#x4E0A;&#xFF0C;&#x662F;&#x4E00;&#x500B;Playground&#xFF0C;&#x6B61;&#x8FCE;&#x4E0B;&#x8F09;&#x4F86;&#x73A9;&#x73A9;&#x770B;&#xFF0C;&#x9019;&#x500B;Playground&#x53E6;&#x5916;&#x591A;&#x5BE6;&#x505A;&#x4E86;&#x4E00;&#x500B;test&#xFF0C;&#x662F;&#x9A57;&#x8B49;get&#x662F;&#x4E0D;&#x662F;&#x6709;&#x6B63;&#x78BA;&#x5730;&#x628A;&#x8CC7;&#x6599;&#x900F;&#x904E;callback&#x56DE;&#x50B3;&#x56DE;&#x4F86;&#xFF0C;&#x53EF;&#x4EE5;&#x770B;&#x770B;&#x5B83;&#x8209;&#x4E00;&#x53CD;&#x4E09;&#xFF01;</p><p>&#x5BEB;&#x6E2C;&#x8A66;&#x8981;&#x6709;&#x500B;&#x5FC3;&#x7406;&#x6E96;&#x5099;&#xFF0C;&#x5C31;&#x662F;&#x9700;&#x8981;&#x5169;&#x500D;&#x4EE5;&#x4E0A;&#x7684;&#x524D;&#x671F;&#x958B;&#x767C;&#x6642;&#x9593;&#xFF0C;&#x4E26;&#x4E14;&#x5B83;&#x4E0D;&#x662F;&#x842C;&#x9748;&#x55AE;&#xFF0C;&#x4F60;&#x4E0D;&#x6703;&#x56E0;&#x70BA;&#x6709;&#x4E86;Unit Test&#xFF0C;&#x4F60;&#x5C31;&#x6210;&#x70BA;Bug-free man&#xFF0C;&#x5C31;&#x50CF;&#x4F60;&#x4E0D;&#x6703;&#x56E0;&#x70BA;&#x6709;&#x4E86;&#x7A69;&#x5B9A;&#x7684;&#x6536;&#x5165;&#xFF0C;&#x5C31;&#x4E00;&#x5B9A;&#x4EA4;&#x5F97;&#x5230;&#x5973;&#x670B;&#x53CB;&#x4E00;&#x6A23;&#x3002;&#x4F46;&#x5BEB;&#x6E2C;&#x8A66;&#x7D55;&#x5C0D;&#x662F;&#x4E00;&#x4EF6;&#x503C;&#x5F97;&#x6216;&#x8005;&#x8AAA;&#x9700;&#x8981;&#x88AB;&#x6295;&#x5165;&#x7684;&#x4E8B;&#x60C5;&#xFF0C;&#x4E5F;&#x662F;&#x8B93;&#x4F60;&#x7684;&#x7CFB;&#x7D71;&#x80FD;&#x5920;&#x6C38;&#x7E8C;&#x7684;&#x552F;&#x4E00;&#x95DC;&#x9375;&#x3002;</p><p>&#x6700;&#x5F8C;&#xFF0C;&#x975E;&#x5E38;&#x6B61;&#x8FCE;&#x5927;&#x5BB6;&#x4F86;&#x5E6B;&#x6211;&#x770B;&#x4E00;&#x4E0B;&#x9019;&#x6A23;&#x7684;&#x6E2C;&#x8A66;&#x908F;&#x8F2F;&#x662F;&#x4E0D;&#x662F;&#x6709;&#x554F;&#x984C;&#xFF0C;&#x6216;&#x8005;code&#x6709;&#x90A3;&#x908A;&#x53EF;&#x4EE5;&#x52A0;&#x5F37;&#x7684;&#xFF0C;&#x6C92;&#x6709;&#x6C38;&#x9060;&#x6B63;&#x78BA;&#x7684;code&#xFF0C;&#x5C0F;&#x5F1F;&#x975E;&#x5E38;&#x6A02;&#x610F;&#x4FEE;&#x6539;&#x5404;&#x7A2E;&#x7BC4;&#x4F8B;&#x8DDF;&#x5167;&#x5BB9;&#x3002;&#x6709;&#x9700;&#x8981;&#x8A0E;&#x8AD6;&#x7684;&#x5730;&#x65B9;&#x4E5F;&#x6B61;&#x8FCE;&#x63D0;&#x51FA;&#x5594;&#xFF01;</p><h2 id="bonus">Bonus</h2><p>&#x6700;&#x5F8C;&#x6700;&#x5F8C;&#xFF0C;&#x8EAB;&#x70BA;&#x4E00;&#x500B;&#x4E0D;&#x662F;&#x5F88;&#x5F37;&#x7684;&#x5DE5;&#x7A0B;&#x5E2B;&#xFF0C;&#x5E0C;&#x671B;&#x81EA;&#x5DF1;&#x80FD;&#x5920;&#x6709;&#x66F4;&#x591A;&#x6A5F;&#x6703;&#x4E86;&#x89E3;&#x600E;&#x6A23;&#x67B6;&#x69CB;&#x7A0B;&#x5F0F;&#xFF0C;&#x600E;&#x6A23;&#x5BEB;&#x6E2C;&#x8A66;&#x7B49;&#x7B49;&#xFF0C;&#x5C0F;&#x5F1F;&#x76EE;&#x524D;&#x7684;&#x60F3;&#x6CD5;&#x662F;&#x60F3;&#x627E;&#x4E00;&#x4E9B;&#x7368;&#x7ACB;&#x958B;&#x767C;&#x8005;&#xFF0C;&#x5171;&#x540C;&#x7DAD;&#x8B77;&#x4E00;&#x500B;&#x7C21;&#x55AE;&#x7684;project&#xFF0C;&#x4E26;&#x4E14;&#x4E92;&#x76F8;code review&#xFF0C;&#x6BCF;&#x6B21;&#x7684;commit&#x90FD;&#x5927;&#x6982;&#x4E00;&#x5169;&#x500B;&#x5C0F;&#x6642;&#x7684;&#x91CF;&#xFF0C;&#x7136;&#x5F8C;&#x4E00;&#x5B9A;&#x8981;&#x9644;&#x4E0A;coverage 100%&#x7684;test code&#xFF0C;&#x7136;&#x5F8C;&#x518D;review&#x5F7C;&#x6B64;&#x7684;pull request&#x3002;Project&#x61C9;&#x8A72;&#x6703;&#x662F;&#x7C21;&#x55AE;&#x7684;&#x6293;instagram&#x5716;&#x7684;app&#x4E4B;&#x985E;&#x7684;&#xFF0C;&#x4E0D;&#x80FD;&#x8CE3;&#x9322;&#x7684;&#x90A3;&#x7A2E;XD</p><p>&#x6709;&#x8208;&#x8DA3;&#x7684;&#x5927;&#x5927;&#x5011;&#xFF0C;&#x53EF;&#x4EE5;&#x7559;&#x4E0B;&#x8CC7;&#x6599;&#xFF0C;&#x53EF;&#x4EE5;&#x627E;&#x4E00;&#x5929;&#x4F86;kick off&#x4E00;&#x4E0B;&#xFF01;XD</p><p>Happy coding!</p><figure class="kg-card kg-image-card"><img src="https://koromiko1104.files.wordpress.com/2017/07/giphy.gif" class="kg-image" alt loading="lazy"></figure><h2 id="reference">Reference</h2><p>&#x9019;&#x7BC7;&#x7D55;&#x5927;&#x591A;&#x6578;&#x7684;&#x8CC7;&#x6599;&#x4F86;&#x81EA;&#x65BC;</p><p><a href="http://masilotti.com/testing-nsurlsession-input/">Mocking Classes You Don&apos;t Own</a></p><p>&#x9019;&#x7BC7;&#x8A9E;&#x6CD5;&#x5927;&#x591A;&#x662F;&#x820A;&#x7684;&#xFF0C;&#x4F46;&#x6982;&#x5FF5;&#x662F;&#x6046;&#x4E45;&#x4E0D;&#x8B8A;&#x7684;&#x3002;</p><p>&#x53E6;&#x5916;&#x4E5F;&#x53C3;&#x8003;&#x4E86;</p><p><a href="https://www.objc.io/issues/15-testing/dependency-injection/">Dependency Injection</a></p><p>&#x6587;&#x7AE0;&#x975E;&#x5E38;&#x5DE8;&#x7D30;&#x5F4C;&#x907A;&#x5730;&#x5217;&#x51FA;&#x4E86;&#x5404;&#x7A2E;&#x5728;iOS&#x4E0A;&#x7684;DI&#x6280;&#x5DE7;&#xFF0C;&#x4E5F;&#x5305;&#x62EC;&#x4E86;&#x4E0B;&#x4E00;&#x7BC7;&#x5C0F;&#x5F1F;&#x6703;&#x6574;&#x7406;&#x7684;Coredata Depedency Injection&#xFF0C;&#x503C;&#x5F97;&#x4E00;&#x770B;(&#x4F46;&#x6587;&#x7AE0;&#x771F;&#x7684;&#x5F88;&#x9577;XD)&#x3002;</p><p>&#x9084;&#x6709;&#x4E00;&#x672C;&#x4E0D;&#x932F;&#x7684;&#x66F8;</p><p><a href="https://www.amazon.com/Test-Driven-Development-Swift-Dominik-Hauser/dp/178588073X">Test-Driven iOS Development with Swift</a></p><p>&#x9019;&#x672C;&#x5728;amazon&#x4E0A;&#x8CB7;&#x6BD4;&#x8F03;&#x8CB4;&#xFF0C;&#x53EF;&#x4EE5;&#x53BB;<a href="https://www.packtpub.com/application-development/test-driven-ios-development-swift-3">&#x9019;&#x88E1;</a>&#x8CB7;&#xFF0C;&#x5C0F;&#x5F1F;&#x56E0;&#x70BA;kindle&#x592A;&#x65B9;&#x4FBF;&#x8CB7;&#x7684;&#x6642;&#x5019;&#x5C31;&#x6C92;&#x6709;&#x6BD4;&#x50F9;.....</p><p>&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C;&#x76EE;&#x524D;&#x7E3D;&#x5171;&#x6709;&#x4E09;&#x96C6;&#xFF0C;&#x6B61;&#x8FCE;&#x4E00;&#x4F75;&#x89C0;&#x8CDE;&#xFF01;</p><p><a href="https://koromiko1104.wordpress.com/2017/07/30/unit-test-for-networking/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; Unit Test for Networking</a></p><p><a href="https://koromiko1104.wordpress.com/2017/10/05/unit-test-for-core-data/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; Unit Test for Core Data</a></p><p><a href="https://koromiko1104.wordpress.com/2017/10/05/mvvmapp/">&#x6B61;&#x8FCE;&#x4F86;&#x5230;&#x771F;&#x5BE6;&#x4E16;&#x754C; &#x2013; &#x539F;&#x4F86;&#x662F;&#x90A3;&#x500B;&#x50B3;&#x8AAA;&#x4E2D;&#x7684;MVVM&#x963F;</a></p>]]></content:encoded></item><item><title><![CDATA[Sequence in Swift - A 🍻 Story]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/banner.png" class="kg-image" alt loading="lazy"></figure><p><strong>Generator</strong>&#x8DDF;<strong>Sequence</strong>&#x662F;&#x5728;&#x8A31;&#x591A;&#x8A9E;&#x8A00;&#x4E4B;&#x4E2D;&#x5E38;&#x898B;&#x7684;design pattern&#xFF0C;&#x6709;&#x4E86;&#x9019;&#x5169;&#x7A2E;patterns&#xFF0C;&#x4F60;&#x53EF;&#x4EE5;&#x5F88;&#x6E05;&#x695A;&#x5730;&#x628A;&#x6309;&#x9700;&#x6C42;&#x53D6;&#x8CC7;&#x6599;&#x8DDF;&#x64CD;&#x4F5C;&#x6709;&#x9806;&#x5E8F;&#x7684;&#x8CC7;&#x6599;&#x9019;&#x5169;&#x4EF6;</p>]]></description><link>https://huangshihting.works/blog/sequence-in-swift-a-story/</link><guid isPermaLink="false">61f2a76c56cf0e0001441877</guid><dc:creator><![CDATA[Huang ShihTing]]></dc:creator><pubDate>Tue, 04 Apr 2017 14:08:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://huangshihting.works/blog/content/images/2022/01/banner.png" class="kg-image" alt loading="lazy"></figure><p><strong>Generator</strong>&#x8DDF;<strong>Sequence</strong>&#x662F;&#x5728;&#x8A31;&#x591A;&#x8A9E;&#x8A00;&#x4E4B;&#x4E2D;&#x5E38;&#x898B;&#x7684;design pattern&#xFF0C;&#x6709;&#x4E86;&#x9019;&#x5169;&#x7A2E;patterns&#xFF0C;&#x4F60;&#x53EF;&#x4EE5;&#x5F88;&#x6E05;&#x695A;&#x5730;&#x628A;&#x6309;&#x9700;&#x6C42;&#x53D6;&#x8CC7;&#x6599;&#x8DDF;&#x64CD;&#x4F5C;&#x6709;&#x9806;&#x5E8F;&#x7684;&#x8CC7;&#x6599;&#x9019;&#x5169;&#x4EF6;&#x4E8B;&#x60C5;&#x900F;&#x904E;&#x7A0B;&#x5F0F;&#x5BEB;&#x51FA;&#x4F86;&#x3002;</p><p>Swift&#x5728;2.0&#x6642;&#x5C31;&#x5DF2;&#x7D93;&#x63D0;&#x4F9B;&#x4E86;<strong>generatorType</strong>&#x8DDF;<strong>sequenceType</strong>&#x5169;&#x7A2E;protocol&#x4F86;&#x8B93;&#x4F60;&#x5BE6;&#x4F5C;&#xFF0C;&#x4F46;&#x5728;3.0&#x4E4B;&#x5F8C;&#xFF0C;&#x7D71;&#x4E00;&#x90FD;&#x6539;&#x6210;&#x4E86;<strong>IteratorProtocol</strong>&#x8DDF;<strong>Sequence</strong>&#xFF0C;&#x6240;&#x6709;&#x8DDF;generator&#x6709;&#x95DC;&#x7684;&#x547D;&#x540D;&#x4E5F;&#x90FD;&#x6539;&#x6210;&#x4E86;Iterator&#x3002;&#x9019;&#x8B93;&#x9019;&#x5169;&#x8005;&#x7684;&#x95DC;&#x4FC2;&#x66F4;&#x70BA;&#x660E;&#x78BA;&#xFF0C;&#x800C;&#x4E0D;&#x518D;&#x662F;&#x9577;&#x5F97;&#x50CF;&#x7684;&#x5169;&#x500B;&#x5354;&#x5B9A;&#x3002;</p><p>&#x4E0B;&#x9762;&#x5C07;&#x5F9E;<strong>Iterator</strong>&#x958B;&#x59CB;&#xFF0C;&#x5229;&#x7528;&#x7BC4;&#x4F8B;&#x4F86;&#x5BE6;&#x505A;&#x4E00;&#x500B;Swift&#x7684;<strong>Sequence</strong>&#x3002;</p><h3 id="%E8%83%8C%E6%99%AF">&#x80CC;&#x666F;</h3><p>&#x73FE;&#x5728;&#x8981;&#x5BE6;&#x505A;&#x4E00;&#x53F0;&#x667A;&#x6167;&#x578B;&#x5564;&#x9152;&#x8CA9;&#x8CE3;&#x6A5F;&#xFF0C;&#x8CA9;&#x8CE3;&#x6A5F;&#x4F9D;&#x5E8F;&#x88DD;&#x8457;&#x4E00;&#x7F50;&#x4E00;&#x7F50;&#x7684;&#x5564;&#x9152;&#x3002;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x8CA9;&#x8CE3;&#x6A5F;&#x7684;UI&#x80FD;&#x5920;&#xFF1A;</p><ol><li>&#x6309;&#x7167;&#x5BB9;&#x91CF;&#x5217;&#x51FA;&#x6A5F;&#x5668;&#x88E1;&#x7684;&#x5564;&#x9152;</li><li>&#x53EA;&#x5217;&#x51FA;&#x5927;&#x65BC;400ml&#x7684;&#x5564;&#x9152;</li><li>&#x6574;&#x53F0;&#x6A5F;&#x5668;&#x7684;&#x7E3D;&#x85CF;&#x9152;&#x6BEB;&#x5347;&#x6578;</li></ol><p>&#x57FA;&#x672C;&#x985E;&#x5225;&#x662F;&#x7576;&#x7136;&#x662F;<strong>Beer</strong> &#x1F37B;&#x3002;</p><pre><code class="language-swift">struct Beer {
	var brandName: String    //&#x54C1;&#x724C;
	var volume: Int               //&#x5BB9;&#x91CF;
}
</code></pre><p>&#x63A5;&#x4E0B;&#x4F86;&#x6211;&#x5011;&#x8981;&#x4E00;&#x6B65;&#x4E00;&#x6B65;&#x900F;&#x904E;<strong>Sequence</strong>&#x5B8C;&#x6210;&#x9019;&#x53F0;&#x8CA9;&#x8CE3;&#x6A5F;&#x7684;&#x958B;&#x767C;&#x3002;&#x9996;&#x5148;&#xFF0C;&#x6211;&#x5011;&#x9700;&#x8981;&#x4E00;&#x500B;&#x6838;&#x5FC3;&#x7D50;&#x69CB;<code>BeerContainer</code>&#x4F86;&#x88DD;&#x6240;&#x6709;&#x7684;&#x5564;&#x9152;&#xFF1A;</p><pre><code class="language-swift">struct BeerContainer {
	let elements: [Beer]
	var i = 0

	init(elements: [Beer]) {
		self.elements = elements
	}
}
</code></pre><p>&#x8B8A;&#x6578;<code>elements</code>&#x662F;&#x4E00;&#x500B;Array&#xFF0C;&#x653E;&#x8457;&#x6211;&#x5011;&#x7684;&#x1F37B;&#x3002;&#x6211;&#x5011;&#x5E0C;&#x671B;&#x9019;&#x500B;container&#x4E0D;&#x53EA;&#x80FD;&#x5B58;&#x6771;&#x897F;&#xFF0C;&#x9084;&#x80FD;&#x5728;&#x6211;&#x5011;&#x9700;&#x8981;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x4E00;&#x7B46;&#x4E00;&#x7B46;&#x5730;&#x628A;&#x5564;&#x9152;&#x5217;&#x51FA;&#x4F86;&#x3002;&#x9019;&#x6A23;&#x7684;&#x884C;&#x70BA;&#x6A21;&#x5F0F;&#x5728;Swift&#x5C31;&#x53EB;&#x505A;Iterator&#xFF0C;&#x6307;&#x7684;&#x5C31;&#x662F;&#x5728;&#x904D;&#x6B77;&#x67D0;&#x500B;&#x5BB9;&#x5668;&#x6642;&#xFF0C;&#xFF0C;&#x4E0D;&#x662F;&#x5148;&#x628A;&#x5BB9;&#x5668;&#x88E1;&#x7684;&#x7269;&#x4EF6;&#x5168;&#x90E8;&#x6524;&#x958B;&#x8B93;&#x4F60;&#x53BB;traverse&#xFF0C;&#x800C;&#x662F;&#x5728;&#x4E0B;<code>next()</code>&#x4E4B;&#x5F8C;&#xFF0C;&#x624D;&#x53BB;&#x8A08;&#x7B97;&#x5B83;&#x7684;&#x4E0B;&#x4E00;&#x7B46;&#x8CC7;&#x6599;&#x4E26;&#x56DE;&#x50B3;&#x3002;&#x60F3;&#x8981;&#x8B93;&#x6211;&#x5011;&#x7684;<strong>BeerContainer</strong>&#x80FD;&#x5920;&#x6210;&#x70BA;&#x4E00;&#x500B;Iterator&#xFF0C;&#x5C31;&#x9700;&#x8981;&#x5BE6;&#x4F5C;<strong>IteratorProtocol</strong>&#x3002;</p><h3 id="iteratorprotocol">IteratorProtocol</h3><p>IteratorProtocol&#x9577;&#x9019;&#x6A23;&#xFF1A;</p><pre><code class="language-swift">protocol IteratorProtocol {
	associatedtype Element
	mutating func next() -&gt; Element?
}
</code></pre><p>&#x6211;&#x5011;&#x9700;&#x8981;&#x5BE6;&#x4F5C;&#x7684;method&#x5C31;&#x53EA;&#x6709;&#x4E00;&#x500B;&#xFF0C;&#x5C31;&#x662F;<code>next()</code>&#x3002;&#x9019;&#x500B;<code>next()</code>&#x9700;&#x8981;&#x5728;&#x6BCF;&#x6B21;&#x88AB;&#x547C;&#x53EB;&#x7684;&#x6642;&#x5019;&#xFF0C;&#x90FD;&#x56DE;&#x50B3;&#x4E0B;&#x4E00;&#x7B46;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x628A;&#x9019;&#x6B21;&#x7684;&#x8CC7;&#x6599;&#x8A18;&#x4E0B;&#x4F86;&#xFF0C;&#x8B93;&#x4E0B;&#x6B21;&#x547C;&#x53EB;next()&#x6642;&#x80FD;&#x5920;&#x6210;&#x529F;&#x6293;&#x5230;&#x518D;&#x4E0B;&#x4E00;&#x7B46;&#x8CC7;&#x6599;&#x3002;&#x5728;&#x9019;&#x908A;&#x7684;&#x4F8B;&#x5B50;&#xFF0C;&#x6211;&#x5011;&#x5229;&#x7528;<strong>BeerContainer</strong>&#x7684;&#x8B8A;&#x6578;<code>i</code>&#x4F86;&#x4EE3;&#x8868;&#x6211;&#x5011;&#x76EE;&#x524D;&#x6240;&#x5728;&#x8CC7;&#x6599;&#x7684;&#x4F4D;&#x7F6E;&#xFF0C;&#x6BCF;call&#x4E00;&#x6B21;<code>next()</code>&#xFF0C;&#x6211;&#x5011;&#x90FD;&#x8B93;<code>i</code>&#x52A0;&#x4E00;&#xFF0C;&#x9019;&#x6A23;&#x5C31;&#x53EF;&#x4EE5;&#x4E00;&#x6B21;&#x8F38;&#x51FA;&#x4E00;&#x7B46;&#xFF0C;&#x4E26;&#x4E14;&#x4E00;&#x7B46;&#x4E00;&#x7B46;&#x5F80;&#x4E0B;&#x79FB;&#x3002;</p><pre><code class="language-swift">extension BeerContainer: IteratorProtocol {
	typealias Element = Beer

	mutating func next() -&gt; Element? {
		defer {
			i+=1
		}
		return i&lt;elements.count ? elements[i] : nil
	}
}
</code></pre><p>&#x4E0A;&#x9762;&#x7684;&#x7A0B;&#x5F0F;&#x4E2D;&#x6709;&#x5E7E;&#x500B;&#x9EDE;&#x8981;&#x6CE8;&#x610F;&#xFF1A;</p><ol><li><strong>Element</strong>&#x662F;&#x9019;&#x500B;protocol&#x7684;<strong>associated type</strong>&#xFF0C;&#x5728;&#x5BE6;&#x4F5C;&#x6642;&#x6211;&#x5011;&#x9700;&#x8981;&#x660E;&#x78BA;&#x6307;&#x5B9A;&#x6211;&#x5011;&#x7684;&#x9019;&#x500B;iterator&#x662F;&#x8981;&#x91DD;&#x5C0D;&#x90A3;&#x4E00;&#x7A2E;&#x7269;&#x4EF6;&#x64CD;&#x4F5C;&#x3002;</li><li><code>next()</code>&#x9019;&#x500B;method&#x662F;mutating&#x7684;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#x5728;&#x57F7;&#x884C;&#x9019;&#x500B;method&#x5F8C;&#xFF0C;&#x6703;&#x6539;&#x8B8A;&#x9019;&#x500B;&#x7269;&#x4EF6;&#x88E1;&#x7684;&#x67D0;&#x500B;&#x8B8A;&#x6578;&#xFF0C;&#x9019;&#x9EDE;&#x5E36;&#x51FA;&#x4E86;iterator&#x7684;&#x4E00;&#x500B;&#x975E;&#x5E38;&#x91CD;&#x8981;&#x7684;&#x7279;&#x6027;&#xFF0C;&#x5C31;&#x662F;&#x5B83;&#x672C;&#x8EAB;&#x4E0D;&#x662F;immutable&#x7684;&#xFF0C;&#x6BCF;&#x6B21;call next()&#x90FD;&#x6703;&#x6709;&#x4E0D;&#x4E00;&#x6A23;&#x7684;&#x7D50;&#x679C;&#x3002;</li><li><code>defer</code>&#x662F;&#x4E00;&#x500B;Swift&#x7684;&#x795E;&#x5947;&#x8A9E;&#x6CD5;&#xFF0C;&#x4EE3;&#x8868;&#x7684;&#x662F;&#x4E00;&#x500B;closure&#x6703;&#x5728;&#x9019;&#x500B;scope&#x8DD1;&#x5B8C;&#x4E4B;&#x5F8C;&#xFF0C;&#x624D;&#x57F7;&#x884C;closure&#x88E1;&#x9762;&#x7684;&#x5167;&#x5BB9;&#x3002;</li></ol><p>&#x5728;<code>next()</code>&#x4E4B;&#x4E2D;&#xFF0C;&#x6BCF;&#x6B21;&#x57F7;&#x884C;&#x90FD;&#x6703;&#x56DE;&#x50B3;<code>elements[i]</code>&#xFF0C;&#x4E26;&#x4E14;&#x628A;<code>i</code>&#x52A0;&#x4E00;&#x3002;</p><p>&#x4EE5;&#x4E0B;&#x662F;&#x5BE6;&#x969B;&#x4E0A;&#x7DDA;&#x7684;&#x72C0;&#x6CC1;&#xFF1A;</p><pre><code class="language-swift">let aOrionBeer = Beer(brandName: &quot;Orion&quot;, volume: 300)
let aSaporoBeer = Beer(brandName: &quot;Saporo&quot;, volume: 380)
let aTaiwanBeer = Beer(brandName: &quot;TaiwanBeer&quot;, volume: 330)
let aAsahiBeer = Beer(brandName: &quot;Asahi&quot;, volume: 420)


var aBeerContainer = BeerContainer(elements: [ aOrionBeer, aSaporoBeer, aTaiwanBeer, aAsahiBeer ])

// i=0
print(aBeerContainer.next())
// Orion: 300 ml, i=1

print(aBeerContainer.next())
// Saporo: 380 ml, i=2

print(aBeerContainer.next())
// TaiwanBeer: 330 ml, i=3

print(aBeerContainer.next())
// Asahi: 400 ml, i=4

print(aBeerContainer.next())
// nil, i=5
</code></pre><p>&#x6BCF;&#x6B21;&#x547C;&#x53EB;<code>next()</code>&#xFF0C;&#x9019;&#x500B;iterator&#x624D;&#x6703;&#x56DE;&#x50B3;&#x76EE;&#x524D;<code>i</code>&#x6307;&#x5411;&#x7684;&#x503C;&#xFF0C;&#x4E26;&#x4E14;&#x628A;<code>i</code>&#x52A0;&#x4E00;&#x70BA;&#x4E0B;&#x4E00;&#x6B21;&#x547C;&#x53EB;<code>next()</code>&#x505A;&#x6E96;&#x5099;&#x3002;&#x5728;&#x547C;&#x53EB;&#x7684;&#x904E;&#x7A0B;&#x4E2D;&#xFF0C;<code>i</code>&#x4E00;&#x76F4;&#x90FD;&#x5728;&#x6539;&#x8B8A;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x9019;&#x500B;<strong>BeerContainer</strong>&#x7269;&#x4EF6;&#x672C;&#x8EAB;&#x4E00;&#x76F4;&#x90FD;&#x5728;&#x8B8A;&#x5316;&#xFF0C;&#x7B49;&#x5168;&#x90E8;&#x90FD;&#x53D6;&#x5B8C;&#x4E4B;&#x5F8C;&#xFF0C;&#x5C31;&#x518D;&#x4E5F;&#x62FF;&#x4E0D;&#x5230;&#x503C;&#x4E86;&#x3002;</p><p>&#x4E0A;&#x9762;&#x7684;code&#x53EF;&#x4EE5;&#x5BEB;&#x6210;&#x66F4;&#x5E38;&#x898B;&#x7684;&#x578B;&#x5F0F;&#xFF1A;</p><pre><code class="language-swift">while let aBeer = aBeerMContainer.next() {
	print(aBeer)
}
</code></pre><p>&#x5230;&#x9019;&#x908A;&#x6211;&#x5011;&#x5C31;&#x4E86;&#x89E3;&#x4E86;<strong>IteratorProtocol</strong>&#x8981;&#x600E;&#x6A23;&#x5BE6;&#x4F5C;&#xFF0C;&#x53EF;&#x4EE5;&#x6309;&#x4E00;&#x500B;&#x9215;<code>next()</code>&#x5C31;&#x5674;&#x51FA;&#x4E00;&#x7F50;&#x5564;&#x9152;&#x4E86;&#x1F60E;</p><p>&#x4F46;Iterator&#x53EA;&#x662F;&#x4E00;&#x500B;&#x57FA;&#x672C;&#x7684;&#x8CC7;&#x6599;&#x7D50;&#x69CB;&#xFF0C;&#x5982;&#x679C;&#x6211;&#x5011;&#x60F3;&#x8981;&#x505A;&#x4E00;&#x4E9B;&#x5982;&#x6392;&#x5E8F;&#x3001;&#x904E;&#x6FFE;&#x7B49;&#x7B49;&#x8B8A;&#x5316;&#xFF0C;&#x5C31;&#x5FC5;&#x9808;&#x8981;&#x518D;&#x628A;Iterator&#x6253;&#x5305;&#x6210;&#x66F4;&#x9AD8;&#x5C64;&#x7684;&#x8CC7;&#x6599;&#x7D50;&#x69CB;&#xFF1A;<strong>Sequence</strong>&#x3002;</p><h2 id="sequence-protocol">Sequence protocol</h2><p>&#x5728;Swift&#x4E4B;&#x4E2D;&#xFF0C;Sequence&#x4EE3;&#x8868;&#x7684;&#x662F;&#x4E00;&#x500B;&#x6709;&#x5E8F;&#x7684;&#x8CC7;&#x6599;&#x7D50;&#x69CB;&#x3002;&#x50CF;&#x6211;&#x5011;&#x5E38;&#x898B;&#x7684;Array&#x5C31;&#x6709;&#x7B26;&#x5408;Sequence&#xFF0C;&#x53EF;&#x4EE5;&#x7528;for..in&#x7684;&#x65B9;&#x6CD5;&#x4F86;&#x53D6;&#x7528;&#x5167;&#x5BB9;&#xFF1A;</p><pre><code class="language-swift">let bugs = [&quot;Aphid&quot;, &quot;Bumblebee&quot;, &quot;Cicada&quot;, &quot;Damselfly&quot;, &quot;Earwig&quot;]
for bug in bugs {
	print(bug)
}
</code></pre><p>&#x4F60;&#x53EF;&#x4EE5;&#x628A;Sequence&#x7406;&#x89E3;&#x6210;&#x50CF;Array&#x4E00;&#x6A23;&#x7684;&#x6709;&#x5E8F;&#x7D50;&#x69CB;&#xFF0C;&#x800C;Iterator&#x6BD4;&#x8F03;&#x50CF;&#x662F;&#x4E00;&#x500B;&#x9700;&#x8981;&#x547C;&#x53EB;&#x624D;&#x6703;&#x6709;&#x52D5;&#x975C;&#x7684;&#x6307;&#x6A19;&#x3002;</p><p>&#x63A5;&#x4E0B;&#x4F86;&#xFF0C;&#x6211;&#x5011;&#x8981;&#x4F86;&#x52D5;&#x624B;&#x88FD;&#x4F5C;&#x6211;&#x5011;&#x7684;<strong>VendorMachine</strong>&#x4E86;&#xFF01;</p><pre><code class="language-swift">struct VendorMachine {
	let elements: [Beer]
}
</code></pre><p>&#x597D;&#x7684;&#xFF0C;&#x73FE;&#x5728;&#x9019;&#x53F0;&#x8CA9;&#x8CE3;&#x6A5F;&#x6BEB;&#x7121;&#x7591;&#x554F;&#xFF0C;&#x5C31;&#x662F;&#x4E00;&#x53F0;&#x53EF;&#x4EE5;&#x88DD;&#x6771;&#x897F;&#x7684;&#x6A5F;&#x5668;&#x3002;&#x518D;&#x4F86;&#x6211;&#x5011;&#x9700;&#x8981;&#x8B93;&#x5B83;&#x6210;&#x70BA;Sequence&#xFF0C;&#x624D;&#x80FD;&#x5920;&#x505A;&#x51FA;&#x5C0D;&#x88E1;&#x9762;&#x7684;&#x5564;&#x9152;&#x6392;&#x5E8F;&#x3001;&#x9078;&#x64C7;&#x7B49;&#x7B49;&#x52D5;&#x4F5C;&#x3002;&#x8981;&#x6210;&#x70BA;&#x4E00;&#x500B;Sequence&#x975E;&#x5E38;&#x7C21;&#x55AE;&#xFF0C;&#x53EA;&#x9700;&#x8981;conform&#x4E00;&#x500B;method<code>makeIterator()</code>&#xFF1A;</p><pre><code class="language-swift">protocol Sequence {
	associatedtype Iterator : IteratorProtocol    
	func makeIterator() -&gt; Iterator
}
</code></pre><p>&#x9019;&#x500B;<code>makeIterator()</code>&#x662F;&#x505A;&#x751A;&#x9EBC;&#x7528;&#x7684;&#xFF1F;Sequence&#x7684;&#x6838;&#x5FC3;&#xFF0C;&#x5C31;&#x662F;&#x9019;&#x500B;Iterator&#xFF0C;Sequence&#x9760;&#x8457;Iterator&#x4F86;&#x505A;&#x5230;&#x4F9D;&#x5E8F;&#x8B80;&#x53D6;&#x8CC7;&#x6599;&#xFF0C;&#x4E26;&#x4E14;&#x5728;&#x9019;&#x6A23;&#x7684;&#x57FA;&#x790E;&#x4E4B;&#x4E0A;&#xFF0C;&#x518D;&#x52A0;&#x5165;&#x8A31;&#x591A;&#x65B9;&#x4FBF;&#x4F7F;&#x7528;&#x7684;&#x65B9;&#x6CD5;&#x5982;<code>map()</code>&#x3001;<code>reduce()</code>&#x3001;&#x8DDF;<code>filter()</code>&#x3002;</p><p>&#x8B93;&#x6211;&#x5011;&#x4F86;&#x628A;<strong>VendorMachine</strong>&#x5BE6;&#x4F5C;&#x6210; <strong>Sequence</strong>&#x5427;&#xFF01;&#x4E0B;&#x9762;&#x662F;&#x4E00;&#x500B;&#x975E;&#x5E38;&#x57FA;&#x672C;&#x7684;&#x5BE6;&#x4F5C;&#xFF1A;</p><pre><code class="language-swift">extension VendorMachine: Sequence {
	typealias Iterator = IndexingIterator&lt;[Beer]&gt;

	func makeIterator() -&gt; Iterator {
		return elements.makeIterator()
	}
}
</code></pre><p>typealias&#x7684;&#x4F5C;&#x7528;&#xFF0C;&#x8DDF;&#x6211;&#x5011;&#x5728;IteratorProtocol&#x7AE0;&#x7BC0;&#x63D0;&#x5230;&#x7684;&#x4E00;&#x6A23;&#xFF0C;&#x662F;&#x70BA;&#x4E86;&#x6307;&#x5B9A;&#x5BE6;&#x4F5C;&#x578B;&#x5225;&#x7528;&#x7684;&#x3002;&#x5728;&#x9019;&#x500B;&#x5BE6;&#x4F5C;&#x4E2D;&#xFF0C;&#x6211;&#x5011;&#x53D6;&#x4E86;&#x500B;&#x6377;&#x5F91;&#xFF0C;&#x76F4;&#x63A5;&#x4F7F;&#x7528;Array&#x4E2D;&#x5DF2;&#x7D93;&#x5B9A;&#x7FA9;&#x597D;&#x7684;<strong>IndexingIterator</strong>&#x4F86;&#x7576;&#x505A;&#x6211;&#x5011;&#x7684;Iterator&#x3002;<strong>IndexingIterator</strong>&#x5C31;&#x662F;&#x4E00;&#x500B;&#x6703;&#x6309;&#x7167;index=1, 2, 3, 4...&#x4F86;&#x8B80;&#x53D6;&#x8CC7;&#x6599;&#x7684;Iterator&#xFF0C;&#x5176;&#x5BE6;&#x529F;&#x80FD;&#x7B49;&#x540C;&#x65BC;&#x6211;&#x5011;&#x4E0A;&#x9762;&#x7684;<strong>BeerContainer</strong>&#x3002;&#x5229;&#x7528;&#x9019;&#x500B;&#x65E2;&#x6709;&#x7684;Iterator&#xFF0C;&#x6211;&#x5011;&#x8A2D;&#x5B9A;&#x6211;&#x5011;&#x7684;Sequence&#xFF0C;&#x8981;&#x5229;&#x7528;&#x9019;&#x500B;Iterator&#x4F86;&#x57F7;&#x884C;Sequence&#x7684;&#x5176;&#x5B83;method&#x3002;&#x7D42;&#x65BC;&#x53EF;&#x4EE5;&#x4F86;&#x5BE6;&#x4F5C;&#x6211;&#x5011;&#x7684;&#x5564;&#x9152;&#x8CA9;&#x8CE3;&#x6A5F;&#x4E86;&#xFF01;</p><p>&#x9996;&#x5148;&#x5148;&#x5EFA;&#x7ACB;&#x597D;&#x6211;&#x5011;&#x7684;<strong>VendorMachine</strong>&#xFF0C;&#x4E26;&#x4E14;&#x585E;&#x4E00;&#x4E9B;&#x5564;&#x9152;&#x9032;&#x53BB;&#xFF1A;</p><pre><code class="language-swift">let aMachine = VendorMachine(elements: [ aOrionBeer, aSaporoBeer, aTaiwanBeer, aAsahiBeer ])
</code></pre><p>&#x9084;&#x8A18;&#x5F97;&#x6211;&#x5011;&#x7684;&#x9700;&#x6C42;&#x55CE;&#xFF1F;</p><ol><li>&#x6309;&#x7167;&#x5BB9;&#x91CF;&#x5217;&#x51FA;&#x6A5F;&#x5668;&#x88E1;&#x7684;&#x5564;&#x9152;</li></ol><p>&#x9019;&#x908A;&#x6211;&#x5011;&#x8981;&#x505A;&#x7684;&#x5C31;&#x662F;&#x628A;<strong>VendorMachine</strong>&#x88E1;&#x7684;&#x6771;&#x897F;&#x6392;&#x5E8F;&#xFF0C;&#x8EAB;&#x70BA;&#x4E00;&#x500B;Sequence&#xFF0C;Swift&#x6709;&#x63D0;&#x4F9B;<code>sorted()</code>&#x8B93;Sequence&#x80FD;&#x5920;&#x56DE;&#x50B3;&#x6392;&#x5E8F;&#x904E;&#x7684;&#x8CC7;&#x6599;&#xFF0C;&#x4F7F;&#x7528;&#x65B9;&#x5F0F;&#x5982;&#x4E0B;&#xFF1A;</p><pre><code class="language-swift">let sortedBeers = aMachine.sorted { $0.volume&gt;$1.volume }
// [Asahi: 420 ml, Saporo: 380 ml, TaiwanBeer: 330 ml, Orion: 300 ml]
</code></pre><p>sorted&#x9019;&#x500B;method&#x7528;&#x4E86;&#x4E0D;&#x5C11;Swift syntactic sugar&#xFF0C;&#x5982;&#x679C;&#x4E0D;&#x719F;&#x7684;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x5C0F;&#x5F1F;&#x62D9;&#x4F5C;<a href="https://koromiko1104.wordpress.com/2017/03/29/swift-syntactic-sugar/">Swift Syntactic Sugar &#x504F;&#x65B9;&#x53EF;&#x6065;&#x4F46;&#x6709;&#x7528;</a>&#xFF0C;&#x61F6;&#x5F97;&#x8AAA;&#x660E;&#x5C31;&#x7528;&#x5DE5;&#x5546;&#x670D;&#x52D9;&#x53D6;&#x4EE3;&#xFF0C;&#x662F;&#x4E00;&#x500B;blogger&#x61C9;&#x6709;&#x7684;&#x614B;&#x5EA6;&#x3002;</p><p>&#x56DE;&#x5230;&#x6B63;&#x984C;&#xFF0C;sorted&#x9019;&#x500B;method&#x6703;&#x56DE;&#x50B3;&#x6392;&#x5E8F;&#x904E;&#x7684;<strong>Sequence</strong>&#x5167;&#x5BB9;&#xFF0C;&#x6392;&#x5E8F;&#x7684;&#x65B9;&#x5F0F;&#x662F;&#x751A;&#x9EBC;&#xFF1F;&#x5C31;&#x662F;&#x5229;&#x7528;sorted&#x552F;&#x4E00;&#x7684;&#x53C3;&#x6578;<code>by</code>&#xFF0C;&#x5B83;&#x662F;&#x4E00;&#x500B;&#x56DE;&#x50B3;Bool&#x7684;closure&#xFF0C;sorted method&#x6703;&#x4F9D;&#x7167;&#x9019;&#x908A;&#x56DE;&#x50B3;&#x7684;Bool&#x4F86;&#x5224;&#x65B7;&#x5169;&#x500B;&#x5143;&#x7D20;&#x4E4B;&#x9593;&#x7684;&#x5927;&#x5C0F;&#x95DC;&#x4FC2;&#x3002;</p><ol><li>&#x53EA;&#x5217;&#x51FA;&#x5927;&#x65BC;400ml&#x7684;&#x5564;&#x9152;</li></ol><p>&#x9019;&#x88E1;&#x5247;&#x662F;&#x6703;&#x4F7F;&#x7528;Sequence&#x5B9A;&#x7FA9;&#x597D;&#x7684;<code>filter()</code>&#x9019;&#x500B;method&#xFF0C;&#x9019;&#x500B;method&#x53EA;&#x6709;&#x4E00;&#x500B;closure&#x53C3;&#x6578;&#xFF0C;&#x5229;&#x7528;closure&#x56DE;&#x50B3;&#x7684;Bool&#xFF0C;&#x4F86;&#x6C7A;&#x5B9A;&#x90A3;&#x4E9B;&#x5143;&#x7D20;&#x8981;&#x7559;&#x4E0B;&#xFF1A;</p><pre><code class="language-swift">let largeBeers = aMachine.filter { $0.volume&gt;400 }
// [Asahi: 420 ml]
</code></pre><ol><li>&#x6574;&#x53F0;&#x6A5F;&#x5668;&#x7684;&#x7E3D;&#x85CF;&#x9152;&#x6BEB;&#x5347;&#x6578;</li></ol><p>&#x76F8;&#x4FE1;&#x5927;&#x5BB6;&#x5C0D;reduce&#x90FD;&#x5DF2;&#x7D93;&#x5F88;&#x719F;&#x6089;&#x4E86;&#xFF0C;&#x4E0D;&#x719F;&#x7684;&#x4E00;&#x6A23;&#x53EF;&#x4EE5;&#x53C3;&#x8003;&#x4E0A;&#x9762;&#x63D0;&#x5230;&#x7684;&#x62D9;&#x4F5C;(&#x4E0D;&#x653E;&#x68C4;&#x6253;&#x6B4C;)&#x3002;&#x4E0B;&#x9762;&#x9019;&#x500B;&#x7A0B;&#x5F0F;&#x5C31;&#x5229;&#x7528;reduce&#xFF0C;&#x628A;&#x6240;&#x6709;&#x7684;volume&#x90FD;&#x7E3D;&#x548C;&#x8D77;&#x4F86;&#x4E26;&#x4E14;&#x8F38;&#x51FA;&#xFF1A;</p><pre><code class="language-swift">let totalVolume = aMachine.reduce(0) { return $0+$1.volume }
// 1430
</code></pre><p>&#x4EE5;&#x4E0A;&#x5C31;&#x662F;&#x4E00;&#x500B;<strong>Sequence</strong>&#x6240;&#x6703;&#x6709;&#x7684;&#x57FA;&#x672C;&#x529F;&#x80FD;&#xFF0C;&#x800C;&#x6211;&#x5011;&#x4E5F;&#x5229;&#x7528;&#x9019;&#x4E9B;&#x57FA;&#x672C;&#x529F;&#x80FD;&#x5B8C;&#x6210;&#x4E86;&#x6211;&#x5011;&#x7684;&#x4EFB;&#x52D9;&#xFF01;&#x1F37B; &#x1F37B; &#x1F37B;</p><p>&#x7B49;&#x7B49;&#xFF0C;&#x9019;&#x500B;&#x6771;&#x897F;&#x8DDF;&#x5B58;&#x6210;<code>[ Beer ]</code>&#x6709;&#x751A;&#x9EBC;&#x4E0D;&#x4E00;&#x6A23;&#xFF1F;&#x6C92;&#x932F;&#xFF0C;&#x6B63;&#x5982;&#x51B0;&#x96EA;&#x8070;&#x660E;&#x7684;&#x4F60;&#x6240;&#x731C;&#x5230;&#x7684;&#xFF0C;&#x9019;&#x5B8C;&#x5168;&#x5C31;&#x662F;&#x4E00;&#x500B;Array&#x7684;&#x7C21;&#x6613;&#x5BE6;&#x4F5C;&#xFF0C;&#x4E26;&#x4E14;&#x6211;&#x5011;&#x9084;&#x53D6;&#x5DE7;&#x5730;&#x7528;&#x4E86;Array&#x7684;Iterator&#x4F86;&#x9054;&#x5230;&#x6211;&#x5011;&#x7684;&#x76EE;&#x7684;&#xFF0C;&#x4F86;&#x6478;&#x8457;&#x4F60;&#x7684;&#x826F;&#x5FC3;&#xFF0C;&#x8ACB;&#x554F;&#x4F60;&#x60F3;&#x9019;&#x6A23;&#x5C31;&#x4EA4;&#x5DEE;&#x55CE;&#xFF1F;&#x60F3;&#xFF01;(&#x5B8C;&#x5168;&#x6C92;&#x8003;&#x616E;)</p><p>&#x63A5;&#x4E0B;&#x4F86;&#x8981;&#x771F;&#x6B63;&#x9032;&#x5165;&#x9019;&#x7BC7;&#x5197;&#x9577;&#x6587;&#x7AE0;&#x7684;&#x4E3B;&#x984C;(&#x662F;&#x6709;&#x591A;&#x5C11;&#x4E3B;&#x984C;)&#xFF0C;&#x5E6B;Sequence&#x88DD;&#x4E0A;&#x81EA;&#x88FD;&#x7684;Iterator&#xFF0C;&#x6210;&#x70BA;&#x771F;&#x6B63;&#x7684;&#x5929;&#x7136;&#x624B;&#x4F5C;Sequence&#x3002;&#x6211;&#x5011;&#x628A;&#x4E0A;&#x9762;&#x7684;implementation&#x6539;&#x6210;&#x4E0B;&#x9762;&#x9019;&#x6A23;&#xFF0C;&#x628A;&#x525B;&#x525B;&#x5BEB;&#x597D;&#x7684;BeerContainer&#x88DD;&#x5230;<code>makeIterator()</code>&#x88E1;&#x9762;&#xFF1A;</p><pre><code class="language-swift">extension VendorMachine: Sequence {
	typealias Iterator = BeerContainer

	func makeIterator() -&gt; Iterator {
		return BeerContainer(elements: self.elements)
	}
}
</code></pre><p>&#x9019;&#x6A23;&#x6211;&#x5011;&#x5C31;&#x5B8C;&#x6210;&#x4E86;&#x6211;&#x5011;&#x7684;&#x8CA9;&#x8CE3;&#x6A5F;&#xFF0C;&#x4E26;&#x4E14;&#x7528;&#x81EA;&#x5DF1;&#x5BEB;&#x597D;&#x7684;Iterator&#x4F86;&#x5BE6;&#x73FE;Sequence&#x4E86;&#xFF01;(&#x4E3B;&#x984C;&#x4E0D;&#x5230;&#x5341;&#x884C;)</p><h2 id="anyiterator">AnyIterator</h2><p>&#x63A5;&#x4E0B;&#x4F86;&#x9032;&#x884C;&#x540C;&#x5834;&#x52A0;&#x6620;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x771F;&#x6B63;&#x5BE6;&#x7528;&#x4E0A;&#x6700;&#x5E38;&#x9047;&#x5230;&#x7684;Sequence&#x8DDF;Iterator&#x642D;&#x914D;&#x7528;&#x6CD5;&#x3002;&#x4E00;&#x822C;&#x5BE6;&#x7528;&#x4E0A;&#x6211;&#x5011;&#x4E0D;&#x592A;&#x6703;&#x70BA;&#x4E86;&#x9019;&#x500B;Sequence&#x7279;&#x5225;&#x7ACB;&#x4E00;&#x500B;Iterator&#xFF0C;&#x800C;&#x662F;&#x6703;&#x5229;&#x7528;&#x4E00;&#x500B;AnyIterator&#xFF0C;&#x4F86;&#x7C21;&#x55AE;&#x5730;&#x628A;makeIterator()&#x7D66;&#x5BE6;&#x4F5C;&#x51FA;&#x4F86;&#x3002;</p><p>AnyIterator&#x662F;&#x4E00;&#x7A2E;Iterator&#xFF0C;&#x5B83;&#x53EF;&#x4EE5;&#x8F38;&#x5165;&#x4E00;&#x500B;closure&#xFF0C;&#x7576;&#x6210;&#x662F;&#x9019;&#x500B;Iterator&#x7684;<code>next()</code>&#x3002;&#x4E0B;&#x9762;&#x7684;&#x7A0B;&#x5F0F;&#x6703;&#x5728;AnyIterator&#x7684;closure&#x4E4B;&#x4E2D;&#xFF0C;&#x5BE6;&#x4F5C;&#x4E00;&#x500B;<em>&#x6BCF;&#x8DD1;&#x4E00;&#x6B21;next()&#x5C31;&#x628A;index&#x52A0;&#x4E00;</em>&#x7684;function&#xFF1A;</p><pre><code class="language-swift">struct VendorMachine: Sequence {
	typealias Iterator = AnyIterator&lt;Beer&gt;

	let elements: [Beer]

	func makeIterator() -&gt; Iterator {
		var i = self.elements.startIndex
		return AnyIterator {
			defer {
				i+=1
			}
			return i&lt;self.elements.count ? self.elements[i] : nil
		}
	}
}
</code></pre><p>&#x4E0A;&#x9762;&#x5C31;&#x662F;&#x628A;&#x6574;&#x500B;iterator&#x5BEB;&#x5728;makeIterator&#x88E1;&#x9762;&#x7684;&#x4F8B;&#x5B50;&#xFF0C;&#x5728;AnyIterator&#x4E4B;&#x4E2D;&#xFF0C;&#x6BCF;&#x8DD1;&#x4E00;&#x6B21;&#x9019;&#x500B;closure&#xFF0C;&#x5C31;&#x6703;&#x56DE;&#x50B3;elements&#x4E4B;&#x4E2D;&#x7B2C;i&#x500B;&#x8B8A;&#x6578;&#xFF0C;&#x4E26;&#x4E14;<code>i</code>&#x6703;&#x88AB;&#x52A0;&#x4E00;&#x3002;&#x9019;&#x6A23;&#x6211;&#x5011;&#x5C31;&#x6210;&#x529F;&#x5730;&#x628A;&#x6574;&#x500B;BeerContainer&#x642C;&#x5230;&#x9019;&#x500B;makeIterator&#x88E1;&#x9762;&#x4E86;&#xFF01;&#x5176;&#x5B83;&#x7684;code&#x90FD;&#x8DDF;&#x6211;&#x5011;&#x5728;BeerContainer&#x4E0A;&#x5BE6;&#x4F5C;&#x7684;&#x4E00;&#x6A23;&#xFF0C;&#x53EA;&#x662F;&#x8981;&#x6CE8;&#x610F;&#x7684;&#x662F;&#xFF0C;AnyIterator&#x7684;&#x53C3;&#x6578;&#x662F;&#x4E00;&#x500B;@escaping closure&#xFF0C;&#x6240;&#x4EE5;&#x5FC5;&#x9808;&#x8981;&#x660E;&#x78BA;&#x5730;&#x628A;<code>self</code>&#x7D66;&#x6A19;&#x51FA;&#x4F86;&#x3002;</p><p>&#x4EE5;&#x4E0A;&#x5C31;&#x662F;Iterator&#x8DDF;Sequence&#x7684;&#x4ECB;&#x7D39;&#xFF0C;&#x6545;&#x4E8B;&#x7684;&#x6700;&#x5F8C;&#xFF0C;&#x8981;&#x4F86;&#x505A;&#x4E00;&#x4E0B;&#x7E3D;&#x7D50;&#xFF1A;</p><ol><li>Iterator&#x5C31;&#x662F;&#x4E00;&#x500B;&#x6309;&#x9700;&#x6C42;&#x4F9D;&#x5E8F;&#x5674;&#x51FA;&#x8CC7;&#x6599;&#x7684;&#x5BB9;&#x5668;&#x3002;</li><li>Sequence&#x5247;&#x662F;&#x5229;&#x7528;Iterator&#x5BE6;&#x4F5C;&#x51FA;&#x4F86;&#x7684;&#x3001;&#x66F4;&#x9AD8;&#x968E;&#x7684;&#x6709;&#x5E8F;&#x8CC7;&#x6599;&#x7D50;&#x69CB;&#x3002;</li><li>&#x5BE6;&#x4F5C;&#x4E86;Sequence&#x4E4B;&#x5F8C;&#xFF0C;&#x5C31;&#x80FD;&#x5920;&#x4F7F;&#x7528;map&#x3001;reduce&#x3001;filter&#x7B49;&#x7B49;method&#x3002;</li><li>AnyIterator&#x80FD;&#x5920;&#x628A;<code>next()</code>&#x62C9;&#x5230;&#x5916;&#x9762;&#x6210;&#x70BA;&#x4E00;&#x500B;&#x53C3;&#x6578;&#xFF0C;&#x65B9;&#x4FBF;Iterator&#x7684;&#x5EFA;&#x7ACB;&#x3002;</li></ol><p>&#x5982;&#x679C;&#x6709;&#x8AA4;&#x7684;&#x8A71;&#x6B61;&#x8FCE;&#x96A8;&#x6642;&#x63D0;&#x51FA;&#x4F86;&#xFF0C;&#x4E5F;&#x6B61;&#x8FCE;&#x8A0E;&#x8AD6;&#x5594;&#xFF01;&#x6240;&#x6709;&#x7684;&#x7A0B;&#x5F0F;&#x78BC;&#x90FD;&#x53EF;&#x4EE5;&#x5728;<a href="https://github.com/koromiko/Tutorial">&#x9019;&#x908A;</a>&#x6293;&#x5F97;&#x5230;&#xFF0C;&#x662F;&#x4E00;&#x500B;Playground&#x3002;</p><p>PS 1. Iterator pattern&#x5F88;&#x5E38;&#x88AB;&#x7528;&#x5728;&#x50CF;&#x8B80;&#x53D6;&#x5927;&#x6A94;&#x6848;&#x9019;&#x7A2E;&#x7121;&#x6CD5;&#x4E00;&#x6B21;&#x5168;&#x90E8;&#x53D6;&#x51FA;&#x4F86;&#xFF0C;&#x53EA;&#x80FD;&#x4E00;&#x7B46;&#x4E00;&#x7B46;&#x5217;&#x7684;&#x60C5;&#x6CC1;&#xFF0C;&#x4E0A;&#x9762;&#x7684;&#x5BE6;&#x4F5C;&#x6709;&#x8A31;&#x591A;&#x66F4;&#x65B9;&#x4FBF;&#x7684;&#x505A;&#x6CD5;&#xFF0C;&#x6211;&#x5011;&#x53EA;&#x662F;&#x5E0C;&#x671B;&#x900F;&#x904E;&#x7C21;&#x5316;&#x4F7F;&#x7528;&#x65B9;&#x5F0F;&#x4F86;&#x89E3;&#x91CB;&#x9019;&#x500B;pattern&#x3002;</p><p>PS 10. Sequence&#x5728;&#x5B9A;&#x7FA9;&#x4E0A;&#xFF0C;&#x4E26;&#x4E0D;&#x80FD;&#x5B8C;&#x5168;&#x7576;&#x6210;&#x50CF;Array&#x4E00;&#x6A23;&#x4F7F;&#x7528;&#xFF0C;&#x751A;&#x9EBC;&#x610F;&#x601D;&#xFF1F;&#x4EE5;&#x4E0B;&#x662F;&#x5B98;&#x65B9;&#x7BC4;&#x4F8B;&#xFF1A;</p><pre><code class="language-swift">for element in sequence {
	if ... some condition { break }
}

for element in sequence {
	// No defined behavior
}
</code></pre><p>&#x7B2C;&#x4E00;&#x500B;for..in&#x5C31;&#x6B63;&#x5E38;&#x4F7F;&#x7528;&#x5B83;&#xFF0C;&#x800C;&#x5728;&#x7B2C;&#x4E8C;&#x500B;for..in&#xFF0C;&#x5982;&#x679C;&#x5C0D;&#x8C61;&#x662F;&#x4E00;&#x500B;Array&#x7684;&#x8A71;&#xFF0C;&#x90A3;&#x5C31;&#x662F;&#x5F9E;&#x982D;&#x5230;&#x5C3E;&#x904D;&#x6B77;&#x4E00;&#x6B21;&#xFF0C;&#x4F46;&#x5982;&#x679C;&#x662F;&#x500B;Sequence&#xFF0C;&#x88E1;&#x9762;&#x7684;&#x884C;&#x70BA;&#x6A21;&#x5F0F;&#x5C31;&#x662F;&#x672A;&#x5B9A;&#x7FA9;&#xFF01;&#x4E5F;&#x5C31;&#x662F;&#x8AAA;&#xFF0C;Swift&#x4E0D;&#x4FDD;&#x8B49;Sequence&#x5728;&#x91CD;&#x8986;&#x8B80;&#x53D6;&#x6642;&#x8CC7;&#x6599;&#x6703;&#x7DAD;&#x6301;&#x4E00;&#x6A23;&#xFF0C;&#x5982;&#x679C;&#x4F60;&#x5E0C;&#x671B;&#x5B83;&#x53EF;&#x4EE5;&#x50CF;Array&#x4E00;&#x6A23;&#x80FD;&#x91CD;&#x8986;&#x5B58;&#x53D6;&#xFF0C;&#x6709;&#x500B;protocol&#x9700;&#x8981;&#x88AB;&#x5BE6;&#x4F5C;&#xFF1A;<code>Collection</code>&#x3002;</p><p>PS 11. Collection&#x6211;&#x5011;&#x5C31;&#x7B49;&#x6625;&#x6696;&#x82B1;&#x958B;&#x6642;&#x518D;&#x4F86;&#x5BEB;&#x5427;....</p><p>PS 100. Norah Jones&#x9019;&#x5C40;&#x5F97;&#x4E00;&#x767E;&#x5206;&#xFF01;&#x8CA2;&#x737B;&#x4E86;&#x9019;&#x7BC7;&#x6587;&#x7AE0;100%&#x7684;&#x80CC;&#x666F;&#x97F3;&#x6A02;&#xFF01;&#x807D;&#x9996;&#x6B4C;&#x5427;&#xFF1A;<a href="https://www.youtube.com/watch?v=ROditq3L8w4">https://www.youtube.com/watch?v=ROditq3L8w4</a></p><p>PS 101. &#x5982;&#x679C;&#x4F60;&#x7684;&#x751F;&#x5B58;&#x4E09;&#x5143;&#x7D20;&#x662F;&#x967D;&#x5149;&#x3001;&#x7A7A;&#x6C23;&#x3001;&#x5564;&#x9152;&#xFF0C;&#x90A3;&#x6B61;&#x8FCE;&#x4F86;&#x5206;&#x4EAB;&#x5728;&#x53F0;&#x5317;&#x7684;&#x6C42;&#x751F;&#x4E4B;&#x8DEF;&#x3002;</p><h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">&#x53C3;&#x8003;&#x8CC7;&#x6599;</h2><p><a href="https://developer.apple.com/reference/swift/sequence#protocol-requirements">https://developer.apple.com/reference/swift/sequence#protocol-requirements</a></p><p><a href="https://medium.com/swift-programming/swift-sequences-ce22d76f120c">https://medium.com/swift-programming/swift-sequences-ce22d76f120c</a></p><p><a href="http://nshipster.com/swift-collection-protocols/">http://nshipster.com/swift-collection-protocols/</a></p><p><a href="https://www.raywenderlich.com/139591/building-custom-collection-swift">https://www.raywenderlich.com/139591/building-custom-collection-swift</a></p><p><a href="https://www.objc.io/books/advanced-swift/preview/#sequence">https://www.objc.io/books/advanced-swift/preview/#sequence</a></p>]]></content:encoded></item></channel></rss>