beta应用程序ios
by Khoa Pham
通过Khoa Pham
I started iOS development when iOS 7 had been announced. And I have learned a bit, through working, advice from colleagues and the iOS community.
我宣布发布iOS 7后就开始了iOS开发。 通过工作,我从同事和iOS社区中获得了一些建议。
In this article, I’d like to share a lot of good practices by taking the example of a simple recipes app. The source code is at GitHub Recipes.
在本文中,我想以一个简单的食谱应用程序为例,分享许多好的做法。 源代码位于GitHub Recipes上 。
The app is a traditional master detail application that showcases a list of recipes together with their detailed information.
该应用程序是传统的主要详细信息应用程序,可显示食谱列表及其详细信息。
There are thousands of ways to solve a problem, and the way a problem is tackled also depends on personal taste. Hopefully, throughout this article you’ll learn something useful — I did learn a lot when I did this project.
解决问题的方法有数千种,而解决问题的方法也取决于个人品味。 希望在本文中,您将学到一些有用的东西-当我完成这个项目时,我学到了很多东西。
I’ve added links to some keywords where I felt further reading would be beneficial. So definitely check them out. Any feedback is welcome.
我已经添加了一些关键字的链接,使我觉得进一步阅读会有所帮助。 所以一定要检查一下。 欢迎任何反馈。
So let’s get started…
因此,让我们开始吧...
Here is a high level overview of what you’ll be building.
这是您将要构建的内容的高级概述。
Let’s decide on the tool and project settings that we use.
让我们决定使用的工具和项目设置。
At WWDC 2018, Apple introduced Xcode 10 with Swift 4.2. However, at the time of writing, Xcode 10 is still in beta 5. So let’s stick with the stable Xcode 9 and Swift 4.1. Xcode 4.2 has some cool features — you can play with it through this awesome Playground. It does not introduce huge changes from Swift 4.1, so we can easily update our app in the near future if required.
在WWDC 2018上 ,Apple推出了带有Swift 4.2的Xcode 10。 但是,在撰写本文时,Xcode 10仍处于beta5。因此,让我们继续使用稳定的Xcode 9和Swift 4.1。 Xcode 4.2具有一些很酷的功能-您可以在这个很棒的Playground中玩它。 它没有对 Swift 4.1进行重大更改,因此如果需要,我们可以在不久的将来轻松更新我们的应用程序。
You should set the Swift version in the Project setting instead of the target settings. This means all targets in the project share the same Swift version (4.1).
您应该在项目设置中而不是目标设置中设置Swift版本。 这意味着项目中的所有目标共享相同的Swift版本(4.1)。
As of summer 2018, iOS 12 is in public beta 5 and we can’t target iOS 12 without Xcode 10. In this post, we use Xcode 9 and the base SDK is iOS 11. Depending on the requirement and user bases, some apps need to support old iOS versions. Although iOS users tend to adopt new iOS versions faster than those who use Android, there are a some that stay with old versions. According to Apples advice, we need to support the two most recent versions, which are iOS 10 and iOS 11. As measured by the App Store on May 31, 2018, only 5% of users use iOS 9 and prior.
截至2018年夏季,iOS 12处于公开测试版5中,如果没有Xcode 10,我们将无法定位到iOS 12.在本文中,我们使用Xcode 9,基本SDK是iOS 11.根据要求和用户群,某些应用程序需要支持旧的iOS版本。 尽管iOS用户倾向于比使用Android的用户更快地采用新的iOS版本,但有些仍然与旧版本保持一致。 根据Apple的建议,我们需要支持两个最新版本 ,分别为iOS 10和iOS 11.根据2018年5月31日App Store的评估,只有5%的用户使用iOS 9和更低版本。
Targeting new iOS versions means we can take advantages of new SDKs, which Apple engineers improve every year. The Apple developer website has an improved change log view. Now it is easier to see what has been added or modified.
定位新的iOS版本意味着我们可以利用新的SDK,Apple工程师每年都会对其进行改进。 Apple开发人员网站具有改进的更改日志视图。 现在,更容易看到已添加或修改的内容。
Ideally, to determine when to drop support for old iOS versions, we need analytics about how users use our app.
理想情况下,要确定何时放弃对旧iOS版本的支持,我们需要有关用户如何使用我们的应用程序的分析。
When we create the new project, select both “Include Unit Tests” and “Include UI Tests” as it a recommended practice to write tests early. Recent changes to the XCTest framework, especially in UI Tests, make testing a breeze and are pretty stable.
当我们创建新项目时,请选择“包括单元测试”和“包括UI测试”,因为这是尽早编写测试的推荐做法。 XCTest框架的最新更改(尤其是在UI测试中)使测试变得轻而易举,并且非常稳定。
Before adding new files to the project, take a pause and think about the structure of your app. How do we want to organize the files? We have a few options. We can organize files by feature/module or role/types. Each has its pros and cons and I’ll discuss them below.
在将新文件添加到项目之前,请先暂停一下,然后考虑一下应用程序的结构。 我们如何组织文件? 我们有一些选择。 我们可以按功能/模块或角色/类型组织文件。 每个都有其优点和缺点,我将在下面讨论它们。
By role/type:
按角色/类型:
Pros: There is less thinking involved about where to put files. It’s also easier to apply scripts or filters.
优点 :很少考虑将文件放在何处。 应用脚本或过滤器也更加容易。
Cons: It’s hard to correlate if we would want to find multiple files related to the same feature. It would also take time to reorganise files if we want to make them into reusable components in the future.
缺点 :如果要查找与同一功能相关的多个文件,则很难进行关联。 如果我们希望将来将它们变成可重用的组件,则还需要花费一些时间来重新组织文件。
By feature/module
按功能/模块
Pros: It makes everything modular and encourages composition.
优点 :它使一切模块化,并鼓励组合。
Cons: It may get messy when many files of different types are bundled together.
缺点 :将许多不同类型的文件捆绑在一起时,可能会变得混乱。
Personally, I try to organize my code by features/components as much as possible. This makes it easier to identify related code to fix, and to add new features easier in the future. It answers the question “What does this app do?” instead of “What is this file?” Here is a good article regarding this.
就个人而言,我尝试按功能/组件尽可能组织代码。 这样可以更轻松地确定要修复的相关代码,并在将来更轻松地添加新功能。 它回答了“此应用程序做什么?”的问题。 而不是“这是什么文件?” 这是一篇很好的文章 。
A good rule of thumb is to stay consistent, no matter which structure you choose. ?
无论选择哪种结构,一个好的经验法则就是保持一致。 ?
The following is the app structure that our recipe app uses:
以下是我们的食谱应用程序使用的应用程序结构:
Contains source code files, split into components:
包含源代码文件,分为以下部分:
Features: the main features in the app
功能:应用程序中的主要功能
Home: the home screen, showing a list of recipes and an open search
主页:主屏幕,显示食谱列表和打开的搜索
List: shows a list of recipes, including reloading a recipe and showing an empty view when a recipe does not exist
列表:显示配方列表,包括重新加载配方以及不存在配方时显示空白视图
Search: handle search and debouncing
搜索:处理搜索和反跳
Detail: shows detail information
详细信息:显示详细信息
Contains the core components of our application:
包含我们应用程序的核心组件:
Flow: contains FlowController to manage flows
流:包含用于管理流的FlowController
Adapter: generic data source for UICollectionView
适配器: UICollectionView
通用数据源
Extension: convenient extensions for common operations
扩展:用于常见操作的便捷扩展
Model: The model in the app, parsed from JSON
模型:应用中的模型,从JSON解析
Contains plist, resource, and Storyboard files.
包含plist,资源和情节提要文件。
I agree with most of the style guides in raywenderlich/swift-style-guide and github/swift-style-guide. These are straightforward and reasonable to use in a Swift project. Also, check out the official API Design Guidelines made by the Swift team at Apple on how to write better Swift code.
我同意raywenderlich / swift-style-guide和github / swift-style-guide中的大多数样式指南 。 这些在Swift项目中使用起来简单明了。 另外,请查阅Apple的Swift团队制定的官方API设计指南 ,以了解如何编写更好的Swift代码。
Whichever style guide you choose to follow, code clarity must be your most important goal.
无论您选择遵循哪种样式指南, 代码清晰度都必须是您最重要的目标。
Indentation and the tab-space war is a sensitive topic, but again, it depends on taste. I use four spaces indentation in Android projects, and two spaces in iOS and React. In this Recipes app, I follow consistent and easy-to-reason indentation, which I have written about here and here.
缩进和制表符空间之战是一个敏感的话题,但同样,它取决于口味。 我在Android项目中使用了四个空格缩进,在iOS和React中使用了两个空格。 在此Recipes应用程序中,我遵循一致且易于理解的缩进,该缩进是我在此处和此处编写的。
Good code should explain itself clearly so you don’t need to write comments. If a chunk of code is hard to understand, it’s good to take a pause and refactor it to some methods with descriptive names so it’s the chunk of code is more clear to understand. However, I find documenting classes and methods are also good for your coworkers and future self. According to the Swift API design guidelines,
好的代码应该清楚地说明自己,因此您无需编写注释。 如果很难理解一小段代码,那么最好暂停一下并将其重构为具有描述性名称的某些方法,这样一整段代码更容易理解。 但是,我发现记录类和方法对您的同事和未来的自我也有好处。 根据Swift API设计指南 ,
Write a documentation comment for every declaration. Insights gained by writing documentation can have a profound impact on your design, so don’t put it off.
为每个声明编写文档注释 。 通过编写文档获得的见解会对您的设计产生深远的影响,因此请不要拖延。
It’s very easy to generate comment template ///
in Xcode with Cmd+Alt+/
. If you plan to refactor your code to a framework to share with others in the future, tools like jazzy can generate documentation so other people can follow along.
使用Cmd+Alt+/
在Xcode中生成注释模板///
非常容易。 如果您打算将代码重构为一个框架,以便将来与其他人共享,则jazzy之类的工具可以生成文档,以便其他人可以跟随。
The use of MARK
can be helpful to separate sections of code. It also groups functions nicely in the Navigation Bar. You can also use extension
groups, related properties and methods.
MARK
的使用有助于分离代码部分。 它还可以在导航栏中很好地对功能进行分组。 您还可以使用extension
组,相关属性和方法。
For a simple UIViewController
we can possible define the following MARKs:
对于简单的UIViewController
我们可以定义以下MARK:
// MARK: - Init// MARK: - View life cycle// MARK: - Setup// MARK: - Action// MARK: - Data
Git is a popular source control system right now. We can use the template .gitignore
file from gitignore.io/api/swift. There are both pros and cons in checking in dependencies files (CocoaPods and Carthage). It depends on your project, but I tend to not commit dependencies (node_modules, Carthage, Pods) in source control to not clutter the code base. It also makes reviewing Pull requests easier.
Git是当前流行的源代码控制系统。 我们可以使用gitignore.io/api/swift中的模板.gitignore
文件。 签入依赖文件(CocoaPods和Carthage)既有优点也有缺点。 这取决于您的项目,但是我倾向于不提交源代码控制中的依赖项(node_modules,Carthage,Pods),以免使代码库混乱。 这也使查看“拉”请求更加容易。
Whether or not you check in the Pods directory, the Podfile and Podfile.lock should always be kept under version control.
无论是否签入Pods目录,Podfile和Podfile.lock都应始终受版本控制。
I use both iTerm2 to execute commands and Source Tree to view branches and staging.
我既使用iTerm2来执行命令,也使用Source Tree来查看分支和暂存。
I have used third party frameworks, and also made and contributed to open source a lot. Using a framework gives you a boost at the start, but it can also limit you a lot in the future. There may be some trivial changes that are very hard to work around. The same thing happens when using SDKs. My preference is to pick active open source frameworks. Read the source code and check frameworks carefully, and consult with your team if you plan to use them. A bit of extra caution does no harm.
我使用了第三方框架,并为开源做出了很多贡献。 使用框架从一开始就可以助您一臂之力 ,但是将来也会限制您很多。 可能会有一些琐碎的更改很难解决。 使用SDK时也会发生相同的情况。 我的偏好是选择活跃的开源框架。 阅读源代码并仔细检查框架,如果打算使用它们,请咨询您的团队。 格外小心一点也没有害处。
In this app, I try to use as few dependencies as possible. Just enough to demonstrate how to manage dependencies. Some experienced developers may prefer Carthage, a dependency manager as it gives you complete control. Here I choose CocoaPods because its easy to use, and it has worked great so far.
在这个应用程序中,我尝试使用尽可能少的依赖项。 足以演示如何管理依赖项。 一些经验丰富的开发人员可能更喜欢依赖关系管理器Carthage ,因为它可以为您提供完全的控制权。 在这里,我选择CocoaPods是因为它易于使用,并且到目前为止效果很好。
There’s a file called .swift-version
of value 4.1
in the root of the project to tell CocoaPods that this project uses Swift 4.1. This looks simple but took me quite some time to figure out. ☹️
项目根目录中有一个名为.swift-version
的值4.1
文件,用于告诉CocoaPods该项目使用Swift 4.1。 这看起来很简单,但是花了我很多时间才能弄清楚。 ☹️
Let’s craft some launch images and icons to give the project a nice look.
让我们精心制作一些启动图像和图标,以使项目看起来更漂亮。
The easy way to learn iOS networking is through public free API services. Here I use food2fork. You can register for an account at http://food2fork.com/about/api. There are many other awesome APIs in this public-api repository.
学习iOS联网的简单方法是通过免费的公共API服务。 在这里,我使用food2fork。 您可以在http://food2fork.com/about/api上注册一个帐户。 此public-api存储库中还有许多其他很棒的API。
It’s good to keep your credentials in a safe place. I use 1Password to generate and store my passwords.
将您的凭据保存在安全的地方很好。 我使用1Password生成和存储密码。
Before we start coding, let’s play with the APIs to see which kinds of requests they require and responses they return. I use the Insomnia tool to test and analyze API responses. It’s open source, free, and works great. ?
在开始编码之前,让我们玩一下API,看看它们需要哪种请求以及它们返回的响应。 我使用失眠工具测试和分析API响应。 它是开源的,免费的,效果很好。 ?
The first impression is important, so is the Launch Screen. The preferred way is using LaunchScreen.storyboard
instead of a static Launch image.
第一印象很重要,启动屏幕也很重要。 首选方式是使用LaunchScreen.storyboard
而不是静态的启动映像。
To add a launch image to Asset Catalog
, open LaunchScreen.storyboard
, add UIImageView
, and pin it to the edges of UIView
. We should not pin the image to the Safe Area as we want the image to be full screen. Also, unselect any margins in the Auto Layout constraints. Set the contentMode
of the UIImageView
as Aspect Fill
so it stretches with the correct aspect ratio.
要将启动图像添加到Asset Catalog
,请打开LaunchScreen.storyboard
,添加UIImageView
,然后将其固定到UIView
的边缘。 我们不要将图像固定在安全区域,因为我们希望图像全屏显示。 同样,在“自动布局”约束中取消选择任何边距。 将UIImageView
的contentMode
设置为Aspect Fill
以便它以正确的纵横比拉伸。
A good practice is to provide all the necessary app icons for each device that you support, and also for places like Notification, Settings, and Springboard. Make sure each image has no transparent pixels, otherwise it results in a black background. This tip is from Human Interface Guidelines - App Icon.
一个好的做法是为您支持的每个设备以及通知,设置和跳板之类的位置提供所有必需的应用程序图标。 确保每个图像都没有透明像素,否则将导致黑色背景。 本技巧摘自Human Interface Guidelines-App Icon 。
Keep the background simple and avoid transparency. Make sure your icon is opaque, and don’t clutter the background. Give it a simple background so it doesn’t overpower other app icons nearby. You don’t need to fill the entire icon with content.
保持背景简单,避免透明 。 确保图标不透明,并且不要弄乱背景。 给它一个简单的背景,以免影响附近的其他应用程序图标。 您无需在整个图标中填充内容。
We need to design square images with a size greater than 1024 x 1024 so each is able to downscale to smaller images. You can do this by hand, script, or use this small IconGenerator app that I made.
我们需要设计尺寸大于1024 x 1024的正方形图像,以便每个图像都可以缩小为较小的图像。 您可以手动执行脚本操作,也可以使用我制作的这个小型IconGenerator应用程序。
The IconGenerator app can generate icons for iOS in iPhone, iPad, macOS and watchOS apps. The result is the AppIcon.appiconset
that we can drag right into the Asset Catalog. Asset Catalog is the way to go for modern Xcode projects.
IconGenerator应用程序可以在iPhone,iPad,macOS和watchOS应用程序中为iOS生成图标。 结果是AppIcon.appiconset
,我们可以将其拖到资产目录中。 资产目录是现代Xcode项目的必经之路。
Regardless of what platform we develop on, it’s good to have a linter to enforce consistent conventions. The most popular tool for Swift projects is SwiftLint, made by the awesome people at Realm.
无论我们在什么平台上进行开发,最好都有一个liner来执行一致的约定。 Swift项目中最受欢迎的工具是SwiftLint ,它是由Realm的杰出人士制作的。
To install it, add pod 'SwiftLint', '~> 0.
25' to the Podf
ile. It's also a good practice to specify the version of the dependencies so pod inst
all won’t accidentally update to a major version that could break your app. Then add a .swiftlint.
yml with your preferred configuration. A sample configuration can be found here.
要安装它,请将pod 'SwiftLint', '~> 0.
25'添加到he Podf
。 这也是一个很好的做法,指定牛逼,他自愿减排量的依赖关系的离子so pod inst
都不会意外更新到可以打破你的应用程序的一个主要版本。 然后添加a .swiftlint.
yml与您的首选配置。 可以找到一个示例配置。
Finally, add a new Run Script Phrase to execute swiftlint
after compiling.
最后,添加新的“运行脚本短语”以在编译后执行swiftlint
。
I use R.swift to safely manage resources. It can generate type-safe classes to access font, localisable strings, and colors. Whenever we change resource file names, we get compile errors instead of a implicit crash. This prevents us inferring with resources that are actively in use.
我使用R.swift安全地管理资源。 它可以生成类型安全的类以访问字体,可本地化的字符串和颜色。 每当我们更改资源文件名时,都会得到编译错误,而不是隐式崩溃。 这样可以防止我们推断正在积极使用的资源。
imageView.image = R.image.notFound()
Let’s dive into the code, starting with the model, flow controllers and service classes.
让我们从模型,流控制器和服务类开始,深入研究代码。
It may sound boring but clients are just a prettier way to represent the API response. The model is perhaps the most basic thing and we use it a lot in the app. It plays such an important role but there can be some obvious bugs related to malformed models and assumptions about how a model should be parsed that need to be considered.
听起来可能很无聊,但是客户端只是代表API响应的更漂亮的方式。 模型也许是最基本的东西,我们在应用程序中经常使用它。 它起着如此重要的作用,但是可能存在一些与格式错误的模型有关的明显错误,并且需要考虑如何解析模型的假设。
We should test for every model of the app. Ideally, we need automated testing of models from API responses in case the model has changed from the backend.
我们应该测试每种型号的应用程序。 理想情况下,我们需要根据API响应自动测试模型,以防模型从后端更改。
Starting from Swift 4.0, we can conform our model to Codable to easily serialise to and from JSON. Our Model should be immutable:
从Swift 4.0开始,我们可以使模型与Codable兼容,以轻松地与JSON进行序列化。 我们的模型应该是不变的:
struct Recipe: Codable { let publisher: String let url: URL let sourceUrl: String let id: String let title: String let imageUrl: String let socialRank: Double let publisherUrl: URL
enum CodingKeys: String, CodingKey { case publisher case url = "f2f_url" case sourceUrl = "source_url" case id = "recipe_id" case title case imageUrl = "image_url" case socialRank = "social_rank" case publisherUrl = "publisher_url" }}
We can use some test frameworks if you like fancy syntax or an RSpec style. Some third party test frameworks may have issues. I find XCTest
good enough.
如果您喜欢精美的语法或RSpec样式,我们可以使用一些测试框架。 一些第三方测试框架可能有问题。 我发现XCTest
足够好。
import XCTest@testable import Recipes
class RecipesTests: XCTestCase { func testParsing() throws { let json: [String: Any] = [ "publisher": "Two Peas and Their Pod", "f2f_url": "http://food2fork.com/view/975e33", "title": "No-Bake Chocolate Peanut Butter Pretzel Cookies", "source_url": "http://www.twopeasandtheirpod.com/no-bake-chocolate-peanut-butter-pretzel-cookies/", "recipe_id": "975e33", "image_url": "http://static.food2fork.com/NoBakeChocolatePeanutButterPretzelCookies44147.jpg", "social_rank": 99.99999999999974, "publisher_url": "http://www.twopeasandtheirpod.com" ]
let data = try JSONSerialization.data(withJSONObject: json, options: []) let decoder = JSONDecoder() let recipe = try decoder.decode(Recipe.self, from: data)
XCTAssertEqual(recipe.title, "No-Bake Chocolate Peanut Butter Pretzel Cookies") XCTAssertEqual(recipe.id, "975e33") XCTAssertEqual(recipe.url, URL(string: "http://food2fork.com/view/975e33")!) }}
Before, I used Compass as a routing engine in my projects, but over time I’ve found that writing simple Routing code works too.
以前,我在项目中使用Compass作为路由引擎 ,但是随着时间的流逝,我发现编写简单的Routing代码也可以。
The FlowController is used to manage many UIViewController
related components to a common feature. You may want to read FlowController and Coordinator for other use cases and to get a better understanding.
FlowController用于将许多与UIViewController
相关的组件管理为一个通用功能。 您可能需要阅读FlowController和Coordinator以获得其他用例,并获得更好的理解。
There is the AppFlowController
that manages changing rootViewController
. For now it starts the RecipeFlowController
.
有一个AppFlowController
可以管理更改的rootViewController
。 现在,它启动RecipeFlowController
。
window = UIWindow(frame: UIScreen.main.bounds)window?.rootViewController = appFlowControllerwindow?.makeKeyAndVisible()appFlowController.start()
RecipeFlowController
manages (in fact it is) the UINavigationController
, that handles pushing HomeViewController, RecipesDetailViewController, SafariViewController
.
RecipeFlowController
管理(实际上是) UINavigationController
,该UINavigationController
处理推送HomeViewController, RecipesDetailViewController, SafariViewController
。
final class RecipeFlowController: UINavigationController { /// Start the flow func start() { let service = RecipesService(networking: NetworkService()) let controller = HomeViewController(recipesService: service) viewControllers = [controller] controller.select = { [weak self] recipe in self?.startDetail(recipe: recipe) } }
private func startDetail(recipe: Recipe) {} private func startWeb(url: URL) {}}
The UIViewController
can use delegate
or closure
to notify FlowController
about changes or next screens in the flow. For delegate
there may be a need to check when there are two instances of the same class. Here we use closure
for simplicity.
UIViewController
可以使用delegate
或closure
来通知FlowController
流中的更改或下一个屏幕。 对于delegate
,可能需要检查何时有两个相同类的实例。 为了简单起见,这里我们使用closure
。
Auto Layout has been around since iOS 5, it gets better each year. Although some people still have a problem with it, mostly because of confusing breaking constraints and performance, but personally, I find Auto Layout to be good enough.
自iOS 5以来,自动版式就已经存在,并且每年都在不断完善。 尽管有些人仍然有问题,主要是因为混乱的限制条件和性能令人困惑,但是就我个人而言,我认为自动版式已经足够了。
I try to use Auto Layout as much as possible to make an adaptive UI. We can use libraries like Anchors to do declarative and fast Auto Layout. However in this app, we’ll just use the NSLayoutAnchor
since it is from iOS 9. The code below is inspired by Constraint. Remember that Auto Layout in its simplest form involves toggling translatesAutoresizingMaskIntoConstraints
and activating isActive
constraints.
我尝试尽可能多地使用“自动布局”来制作自适应UI。 我们可以使用Anchors之类的库来进行声明式和快速的自动布局。 但是,在此应用中,我们将仅使用NSLayoutAnchor
因为它来自NSLayoutAnchor
。以下代码受Constrain t启发。 请记住,最简单形式的自动版式涉及切换translatesAutoresizingMaskIntoConstraints
和激活isActive
约束。
extension NSLayoutConstraint { static func activate(_ constraints: [NSLayoutConstraint]) { constraints.forEach { ($0.firstItem as? UIView)?.translatesAutoresizingMaskIntoConstraints = false $0.isActive = true } }}
There are actually many other layout engines available on GitHub. To get a sense over which one would be suitable to use, check out the LayoutFrameworkBenchmark.
GitHub上实际上还有许多其他布局引擎。 要了解哪种类型适合使用,请查看LayoutFrameworkBenchmark 。
Architecture is probably the most hyped and discussed topic. I’m a fan of exploring architectures, you can view more posts and frameworks about different architectures here.
建筑可能是最受关注和讨论的话题。 我是探索架构的爱好者,您可以在此处查看有关不同架构的更多文章和框架。
To me, all architectures and patterns define roles for each object and how to connect them. Remember these guiding principles for your choice of architecture:
对我来说,所有架构和模式都为每个对象定义角色以及如何连接它们。 在选择架构时,请记住以下指导原则:
After playing around with many different architectures, with and without Rx, I found out that simple MVC is good enough. In this simple project, there is just UIViewController
with logic encapsulated in helper Service
classes,
在使用和不使用Rx的情况下研究了许多不同的体系结构之后,我发现简单的MVC足够了。 在这个简单的项目中,只有UIViewController
,其逻辑封装在辅助Service
类中,
You may have heard people joking about how massive UIViewController
is, but in reality, there is no massive view controller. It’s just us writing bad code. However there are ways to slim it down.
您可能听说过有人开玩笑说UIViewController
有多大,但实际上,没有大的View Controller。 只是我们在编写错误的代码。 但是有一些方法可以苗条下来。
In the recipes app I use,
在我使用的食谱应用中,
Service
to inject into the view controller to perform a single task
Service
注入到视图控制器执行单个任务
Generic View
to move view and controls declaration to the View
layer
Generic View
可将视图和控件声明移至“ View
层
Child view controller
to compose child view controllers to build more features
Child view controller
组成子视图控制器以构建更多功能
Here is a very good article with 8 tips to slim down big controllers.
这是一篇非常不错的文章,其中包含8个技巧,以精简大型控制器。
The SWIFT documentation mentions that “access control restricts access to parts of your code from code in other source files and modules. This feature enables you to hide the implementation details of your code, and to specify a preferred interface through which that code can be accessed and used.”
SWIFT 文档提到:“访问控制限制了从其他源文件和模块中的代码访问部分代码。 使用此功能,您可以隐藏代码的实现细节,并指定可以访问和使用该代码的首选接口。”
Everything should be private
and final
by default. This also helps the compiler. When seeing a public property, we need to search for it across the project before doing anything further with it. If the property is used only within a class
, making it private
means we don't need to care if it breaks elsewhere.
默认情况下,所有内容均应为private
且final
的。 这也有助于编译器 。 看到公共财产时,我们需要在整个项目中进行搜索,然后再对其进行进一步的处理。 如果该属性仅在一个class
,则将其设置为private
意味着我们无需关心它是否在其他地方中断。
Declare properties as final
where possible.
尽可能声明属性为final
属性。
final class HomeViewController: UIViewController {}
Declare properties as private
or at least private(set)
.
将属性声明为private
或至少private(set)
。
final class RecipeDetailView: UIView { private let scrollableView = ScrollableView() private(set) lazy var imageView: UIImageView = self.makeImageView()}
For properties that can be accessed at a later time, we can declare them as lazy
and can use closure
for fast construction.
对于以后可以访问的属性,我们可以将它们声明为lazy
并且可以使用closure
进行快速构造。
final class RecipeCell: UICollectionViewCell { private(set) lazy var containerView: UIView = { let view = UIView() view.clipsToBounds = true view.layer.cornerRadius = 5 view.backgroundColor = Color.main.withAlphaComponent(0.4)
return view }()}
We can also use make
functions if we plan to reuse the same function for multiple properties.
如果我们计划将相同的函数重用于多个属性,则也可以使用make
函数。
final class RecipeDetailView: UIView { private(set) lazy var imageView: UIImageView = self.makeImageView()
private func makeImageView() -> UIImageView { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true return imageView }}
This also matches advice from Strive for Fluent Usage.
这也与Strive for Fluent Use的建议相匹配。
Begin names of factory methods with “make”, For example, x.makeIterator()
.
工厂方法的名称以“ make” x.makeIterator()
,例如x.makeIterator()
。
Some code syntax is hard to remember. Consider using code snippets to auto generate code. This is supported by Xcode and is the preferred way by Apple engineers when they demo.
一些代码语法很难记住。 考虑使用代码片段自动生成代码。 Xcode支持此功能,这是Apple工程师演示时的首选方式。
if #available(iOS 11, *) { viewController.navigationItem.searchController = searchController viewController.navigationItem.hidesSearchBarWhenScrolling = false} else { viewController.navigationItem.titleView = searchController.searchBar}
I made a repo with some useful Swift snippets that many enjoy using.
我用一些有用的Swift片段制作了一个回购协议 。
Networking in Swift is kind of a solved problem. There are tedious and error-prone tasks like parsing HTTP responses, handling request queues, handling parameter queries. I’ve seen bugs about PATCH requests, lowercased HTTP methods, … We can just use Alamofire. There’s no need to waste time here.
Swift中的联网是一个已解决的问题。 有繁琐且容易出错的任务,例如解析HTTP响应,处理请求队列,处理参数查询。 我已经看到了有关PATCH请求, 小写HTTP方法的错误 ,……我们可以使用Alamofire 。 无需在这里浪费时间。
For this app, since it’s simple and to avoid unnecessary dependencies. We can just use URLSession
directly. A resource usually contains URL, path, parameters and the HTTP method.
对于此应用程序,因为它很简单并且可以避免不必要的依赖关系。 我们可以直接使用URLSession
。 资源通常包含URL,路径,参数和HTTP方法。
struct Resource { let url: URL let path: String? let httpMethod: String let parameters: [String: String]}
A simple network service can just parse Resource
to URLRequest
and tells URLSession
to execute
一个简单的网络服务可以将Resource
解析为URLRequest
并告诉URLSession
执行
final class NetworkService: Networking { @discardableResult func fetch(resource: Resource, completion: @escaping (Data?) -> Void) -> URLSessionTask? { guard let request = makeRequest(resource: resource) else { completion(nil) return nil }
let task = session.dataTask(with: request, completionHandler: { data, _, error in guard let data = data, error == nil else { completion(nil) return }
completion(data) })
task.resume() return task }}
Use dependency injection. Allow caller to specify URLSessionConfiguration
. Here we make use of Swift default parameter to provide the most common option.
使用依赖注入。 允许调用者指定URLSessionConfiguration
。 在这里,我们利用Swift默认参数来提供最常用的选项。
init(configuration: URLSessionConfiguration = URLSessionConfiguration.default) { self.session = URLSession(configuration: configuration)}
I also use URLQueryItem which was from iOS 8. It makes parsing parameters to query items nice and less tedious.
我还使用了来自iOS 8的URLQueryItem 。它使解析参数的查询变得美观而又乏味。
We can use URLProtocol and URLCache to add a stub for network responses or we can use frameworks like Mockingjay which swizzles URLSessionConfiguration
.
我们可以使用URLProtocol和URLCache为网络响应添加一个存根,也可以使用Mockingjay之类的使URLSessionConfiguration URLSessionConfiguration
框架。
I myself prefer using the protocol to test. By using the protocol, the test can create a mock request to provide a stub response.
我自己更喜欢使用协议进行测试。 通过使用该协议,测试可以创建一个模拟请求以提供存根响应。
protocol Networking { @discardableResult func fetch(resource: Resource, completion: @escaping (Data?) -> Void) -> URLSessionTask?}
final class MockNetworkService: Networking { let data: Data init(fileName: String) { let bundle = Bundle(for: MockNetworkService.self) let url = bundle.url(forResource: fileName, withExtension: "json")! self.data = try! Data(contentsOf: url) }
func fetch(resource: Resource, completion: @escaping (Data?) -> Void) -> URLSessionTask? { completion(data) return nil }}
I used to contribute and use a library called Cache a lot. What we need from a good cache library is memory and disk cache, memory for fast access, disk for persistency. When we save, we save to both memory and disk. When we load, if memory cache fails, we load from disk, then update memory again. There are many advanced topics about cache like purging, expiry, access frequency. Have a read about them here.
我过去经常贡献和使用一个名为Cache的库。 一个好的缓存库需要的是内存和磁盘缓存,用于快速访问的内存,用于持久性的磁盘。 保存时,我们同时保存到内存和磁盘。 加载时,如果内存缓存失败,则会从磁盘加载,然后再次更新内存。 关于缓存,有许多高级主题,例如清除,到期,访问频率。 在这里阅读有关它们的信息 。
In this simple app, a homegrown cache service class is enough and a good way to learn how caching works. Everything in Swift can be converted to Data
, so we can just save Data
to cache. Swift 4 Codable
can serialize object to Data
.
在这个简单的应用程序中,一个本地缓存服务类就足够了,并且是学习缓存工作原理的好方法。 Swift中的所有内容都可以转换为Data
,因此我们可以将Data
保存到缓存中。 Swift 4 Codable
可以将对象序列化为Data
。
The code below shows us how to use FileManager
for disk cache.
下面的代码向我们展示了如何使用FileManager
进行磁盘缓存。
/// Save and load data to memory and disk cachefinal class CacheService {
/// For get or load data in memory private let memory = NSCache()
/// The path url that contain cached files (mp3 files and image files) private let diskPath: URL
/// For checking file or directory exists in a specified path private let fileManager: FileManager
/// Make sure all operation are executed serially private let serialQueue = DispatchQueue(label: "Recipes")
init(fileManager: FileManager = FileManager.default) { self.fileManager = fileManager do { let documentDirectory = try fileManager.url( for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) diskPath = documentDirectory.appendingPathComponent("Recipes") try createDirectoryIfNeeded() } catch { fatalError() } }
func save(data: Data, key: String, completion: (() -> Void)? = nil) { let key = MD5(key)
serialQueue.async { self.memory.setObject(data as NSData, forKey: key as NSString) do { try data.write(to: self.filePath(key: key)) completion?() } catch { print(error) } } }}
To avoid malformed and very long file names, we can hash them. I use MD5 from SwiftHash, which gives dead simple usage let key = MD5(key)
.
为了避免格式错误且文件名太长,我们可以对它们进行哈希处理。 我使用SwiftHash的 MD5,它给出了简单的用法let key = MD5(key)
。
Since I design Cache
operations to be asynchronous, we need to use test expectation
. Remember to reset the state before each test so the previous test state does not interfere with the current test. The expectation
in XCTestCase
makes testing asynchronous code easier than ever. ?
由于我将Cache
操作设计为异步的,因此我们需要使用test expectation
。 请记住,在每次测试之前都要重置状态,以使先前的测试状态不会干扰当前的测试。 XCTestCase
的expectation
使测试异步代码比以往更加容易。 ?
class CacheServiceTests: XCTestCase { let service = CacheService()
override func setUp() { super.setUp()
try? service.clear() }
func testClear() { let expectation = self.expectation(description: #function) let string = "Hello world" let data = string.data(using: .utf8)!
service.save(data: data, key: "key", completion: { try? self.service.clear() self.service.load(key: "key", completion: { XCTAssertNil($0) expectation.fulfill() }) })
wait(for: [expectation], timeout: 1) }}
I also contribute to Imaginary so I know a bit about how it works. For remote images, we need to download and cache it, and the cache key is usually the URL of the remote image.
我也为Imaginary做出了贡献,所以我对它的工作原理有所了解。 对于远程映像,我们需要下载并缓存它,缓存键通常是远程映像的URL。
In our recipese app, let’s build a simple ImageService based on our NetworkService
and CacheService
. Basically an image is just a network resource that we download and cache. We prefer composition so we’ll include NetworkService
and CacheService
into ImageService
.
在我们的配方应用程序中,让我们基于NetworkService
和CacheService
构建一个简单的ImageService。 基本上,映像只是我们下载和缓存的网络资源。 我们更喜欢合成,因此我们将NetworkService
和CacheService
到ImageService
。
/// Check local cache and fetch remote imagefinal class ImageService {
private let networkService: Networking private let cacheService: CacheService private var task: URLSessionTask?
init(networkService: Networking, cacheService: CacheService) { self.networkService = networkService self.cacheService = cacheService }}
We usually have UICollectionView
and UITableView
cells with UIImageView
. And since cells are reused, we need to cancel any existing request task
before making a new request.
我们通常将UICollectionView
和UITableView
单元格与UIImageView
。 并且由于单元已被重用,因此我们需要在发出新请求之前取消任何现有的request task
。
func fetch(url: URL, completion: @escaping (UIImage?) -> Void) { // Cancel existing task if any task?.cancel()
// Try load from cache cacheService.load(key: url.absoluteString, completion: { [weak self] cachedData in if let data = cachedData, let image = UIImage(data: data) { DispatchQueue.main.async { completion(image) } } else { // Try to request from network let resource = Resource(url: url) self?.task = self?.networkService.fetch(resource: resource, completion: { networkData in if let data = networkData, let image = UIImage(data: data) { // Save to cache self?.cacheService.save(data: data, key: url.absoluteString) DispatchQueue.main.async { completion(image) } } else { print("Error loading image at \(url)") } })
self?.task?.resume() } })}
Let’s add an extension to UIImageView
to set the remote image from the URL. I use associated object
to keep this ImageService
and to cancel old requests. We make good use of associated object
to attach ImageService
to UIImageView
. The point is to cancel the current request when the request is triggered again. This is handy when the image views are rendered in a scrolling list.
让我们向UIImageView
添加扩展,以从URL设置远程图像。 我使用associated object
来保留此ImageService
并取消旧的请求。 我们充分利用associated object
将ImageService
附加到UIImageView
。 关键是在再次触发请求时取消当前请求。 当图像视图在滚动列表中呈现时,这很方便。
extension UIImageView { func setImage(url: URL, placeholder: UIImage? = nil) { if imageService == nil { imageService = ImageService(networkService: NetworkService(), cacheService: CacheService()) }
self.image = placeholder self.imageService?.fetch(url: url, completion: { [weak self] image in self?.image = image }) }
private var imageService: ImageService? { get { return objc_getAssociatedObject(self, &AssociateKey.imageService) as? ImageService } set { objc_setAssociatedObject( self, &AssociateKey.imageService, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } }}
We use UITableView
and UICollectionView
in almost in every app and almost perform the same thing repeatedly.
我们几乎在每个应用程序中都使用UITableView
和UICollectionView
,并且几乎重复执行相同的操作。
There are many wrappers around UITableView
and UICollection
. Each adds another layer of abstraction, which gives us more power but applies restrictions at the same time.
UITableView
和UICollection
周围有许多包装。 每一个都增加了另一层抽象,这给了我们更多的功能,但同时施加了限制。
In this app, I use Adapter
to get a generic data source, to make a type safe collection. Because, in the end, all we need is to map from the model to the cells.
在此应用程序中,我使用Adapter
获取通用数据源,以进行类型安全的收集。 因为最后,我们所需要做的只是将模型映射到单元。
I also utilize Upstream based on this idea. It’s hard to wrap around UITableView
and UICollectionView
, as many times, it is app specific, so a thin wrapper like Adapter
is enough.
我也根据这个想法利用上游技术。 很难包装UITableView
和UICollectionView
,因为它是特定于应用程序的很多次,因此像Adapter
这样的薄包装Adapter
就足够了。
final class Adapter: NSObject,UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var items: [T] = [] var configure: ((T, Cell) -> Void)? var select: ((T) -> Void)? var cellHeight: CGFloat = 60}
I ditched Storyboard because of many limitations and many issues. Instead, I use code to make views and define constraints. It is not that hard to follow. Most of the boilerplate code in UIViewController
is for creating views and configuring the layout. Let's move those to the view. You can read more about that here.
由于许多限制和许多问题,我放弃了Storyboard。 相反,我使用代码制作视图并定义约束。 并不难遵循。 UIViewController
大多数样板代码用于创建视图和配置布局。 让我们将其移至视图。 你可以在这里阅读更多有关它的内容。
/// Used to separate between controller and viewclass BaseController: UIViewController { let root = T()
override func loadView() { view = root }}
final class RecipeDetailViewController: BaseController {}
The View controller container is a powerful concept. Each view controller has a separation of concern and can be composed together to create advanced features. I have used RecipeListViewController
to manage the UICollectionView
and show a list of recipes.
View控制器容器是一个强大的概念。 每个视图控制器都有一个关注点,可以组合在一起以创建高级功能。 我已经使用RecipeListViewController
来管理UICollectionView
并显示食谱列表。
final class RecipeListViewController: UIViewController { private(set) var collectionView: UICollectionView! let adapter = Adapter() private let emptyView = EmptyView(text: "No recipes found!")}
There is the HomeViewController
which embeds this RecipeListViewController
有一个HomeViewController
嵌入了这个RecipeListViewController
/// Show a list of recipesfinal class HomeViewController: UIViewController {
/// When a recipe get select var select: ((Recipe) -> Void)?
private var refreshControl = UIRefreshControl() private let recipesService: RecipesService private let searchComponent: SearchComponent private let recipeListViewController = RecipeListViewController()}
I try to build components and compose code whenever I can . We see that ImageService
makes use of the NetworkService
and CacheService
, and RecipeDetailViewController
makes use of Recipe
and RecipesService
我会尽可能地构建组件并编写代码。 我们看到ImageService
使用NetworkService
和CacheService
,而RecipeDetailViewController
使用Recipe
和RecipesService
Ideally objects should not create dependencies by themselves. The dependencies should be created outside and passed down from root. In our app the root is AppDelegate
and AppFlowController
so dependencies should start from here.
理想情况下,对象不应自行创建依赖关系。 依赖关系应该在外部创建并从root传递下来。 在我们的应用程序中,根目录为AppDelegate
和AppFlowController
因此依赖项应从此处开始。
Since iOS 9, all apps should adopt App Transport Security
从iOS 9开始,所有应用均应采用应用传输安全性
App Transport Security (ATS) enforces best practices in the secure connections between an app and its back end. ATS prevents accidental disclosure, provides secure default behavior, and is easy to adopt; it is also on by default in iOS 9 and OS X v10.11. You should adopt ATS as soon as possible, regardless of whether you’re creating a new app or updating an existing one.
App Transport Security(ATS)在应用与其后端之间的安全连接中实施最佳实践。 ATS可以防止意外泄露,提供安全的默认行为,并且易于采用; 在iOS 9和OS X v10.11中默认情况下也处于启用状态。 无论您是创建新应用还是更新现有应用,都应尽快采用ATS。
In our app, some images are obtained over an HTTP
connection. We need to exclude it from the security rule, but only for that domain only.
在我们的应用程序中,一些图像是通过HTTP
连接获得的。 我们需要将其从安全规则中排除,但仅限于该域。
NSAppTransportSecurity NSExceptionDomains food2fork.com NSIncludesSubdomains NSExceptionAllowsInsecureHTTPLoads
For the detail screen, we can use UITableView
and UICollectionView
with different cell types. Here, the views should be static. We can stack using UIStackView
. For more flexibility, we can just use UIScrollView
.
对于详细信息屏幕,我们可以将UITableView
和UICollectionView
与不同的单元格类型一起使用。 在这里,视图应该是静态的。 我们可以使用UIStackView
进行堆栈。 为了获得更大的灵活性,我们可以只使用UIScrollView
。
/// Vertically layout view using Auto Layout in UIScrollViewfinal class ScrollableView: UIView { private let scrollView = UIScrollView() private let contentView = UIView()
override init(frame: CGRect) { super.init(frame: frame)
scrollView.showsHorizontalScrollIndicator = false scrollView.alwaysBounceHorizontal = false addSubview(scrollView)
scrollView.addSubview(contentView)
NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: topAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), scrollView.leftAnchor.constraint(equalTo: leftAnchor), scrollView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.leftAnchor.constraint(equalTo: leftAnchor), contentView.rightAnchor.constraint(equalTo: rightAnchor) ]) }}
We pin the UIScrollView
to the edges. We pin the contentView
left and right anchor to self
, while pinning the contentView
top and bottom anchor to UIScrollView
.
我们将UIScrollView
到边缘。 我们将contentView
左右锚点固定到self
,同时将contentView
上下锚点固定到UIScrollView
。
The views inside contentView
have top and bottom constraints, so when they expand, they expand contentView
as well. UIScrollView
uses Auto Layout info from this contentView
to determine its contentSize
. Here is how ScrollableView
is used in RecipeDetailView
.
contentView
内部的视图具有顶部和底部约束,因此在展开时,它们也会展开contentView
。 UIScrollView
使用来自此contentView
自动布局信息来确定其contentSize
。 这是RecipeDetailView
如何使用ScrollableView
的方法。
scrollableView.setup(pairs: [ ScrollableView.Pair(view: imageView, inset: UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)), ScrollableView.Pair(view: ingredientHeaderView, inset: UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)), ScrollableView.Pair(view: ingredientLabel, inset: UIEdgeInsets(top: 4, left: 8, bottom: 0, right: 0)), ScrollableView.Pair(view: infoHeaderView, inset: UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0)), ScrollableView.Pair(view: instructionButton, inset: UIEdgeInsets(top: 8, left: 20, bottom: 0, right: 20)), ScrollableView.Pair(view: originalButton, inset: UIEdgeInsets(top: 8, left: 20, bottom: 0, right: 20)), ScrollableView.Pair(view: infoView, inset: UIEdgeInsets(top: 16, left: 0, bottom: 20, right: 0))])
From iOS 8 onwards, we can use the UISearchController to get a default search experience with the search bar and results controller. We’ll encapsuate search functionality into SearchComponent
so that it can be pluggable.
从iOS 8开始,我们可以使用UISearchController通过搜索栏和结果控制器获得默认的搜索体验。 我们会将搜索功能包含在SearchComponent
以便可插入。
final class SearchComponent: NSObject, UISearchResultsUpdating, UISearchBarDelegate { let recipesService: RecipesService let searchController: UISearchController let recipeListViewController = RecipeListViewController()}
Starting from iOS 11, there ‘s a property called searchController
on the UINavigationItem
which makes it easy to show the search bar on the navigation bar.
从iOS 11开始, UINavigationItem
上有一个名为searchController
属性,可轻松在导航栏上显示搜索栏。
func add(to viewController: UIViewController) { if #available(iOS 11, *) { viewController.navigationItem.searchController = searchController viewController.navigationItem.hidesSearchBarWhenScrolling = false } else { viewController.navigationItem.titleView = searchController.searchBar }
viewController.definesPresentationContext = true}
In this app, we need to disable hidesNavigationBarDuringPresentation
for now, as it is quite buggy. Hopefully it gets resolved in future iOS updates.
在此应用中,我们需要hidesNavigationBarDuringPresentation
禁用hidesNavigationBarDuringPresentation
,因为它存在很多问题。 希望它在将来的iOS更新中得到解决。
Understanding presentation context is crucial for view controller presentation. In search, we use the searchResultsController
.
了解表示上下文对于视图控制器表示至关重要。 在搜索中,我们使用searchResultsController
。
self.searchController = UISearchController(searchResultsController: recipeListViewController)
We need to use definesPresentationContext on the source view controller (the view controller where we add the search bar into). Without this we get the searchResultsController
to be presented over full screen !!!
我们需要在源视图控制器(将搜索栏添加到其中的视图控制器)上使用definePresentationContext 。 没有这个,我们将获得全屏显示的searchResultsController
!
When using the currentContext or overCurrentContext style to present a view controller, this property controls which existing view controller in your view controller hierarchy is actually covered by the new content. When a context-based presentation occurs, UIKit starts at the presenting view controller and walks up the view controller hierarchy. If it finds a view controller whose value for this property is true, it asks that view controller to present the new view controller. If no view controller defines the presentation context, UIKit asks the window’s root view controller to handle the presentation.
当使用currentContext或overCurrentContext样式显示视图控制器时,此属性控制新内容实际上覆盖了视图控制器层次结构中的哪个现有视图控制器。 当发生基于上下文的呈现时,UIKit从呈现的视图控制器开始,并遍历视图控制器层次结构。 如果找到该属性值为true的视图控制器,则要求该视图控制器提供新的视图控制器。 如果没有视图控制器定义演示文稿上下文,则UIKit会要求窗口的根视图控制器处理演示文稿。
The default value for this property is false. Some system-provided view controllers, such as UINavigationController, change the default value to true.
此属性的默认值为false。 某些系统提供的视图控制器(例如UINavigationController)将默认值更改为true。
We should not execute search requests for every key stroke the user types in the search bar. Therefore some kind of throttling is needed. We can use DispatchWorkItem
to encapsulate the action and send it to the queue. Later we can cancel it.
我们不应该对用户在搜索栏中键入的每个击键都执行搜索请求。 因此,需要某种限制。 我们可以使用DispatchWorkItem
封装动作并将其发送到队列。 稍后我们可以取消它。
final class Debouncer { private let delay: TimeInterval private var workItem: DispatchWorkItem?
init(delay: TimeInterval) { self.delay = delay }
/// Trigger the action after some delay func schedule(action: @escaping () -> Void) { workItem?.cancel() workItem = DispatchWorkItem(block: action) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem!) }}
To test Debouncer
we can use XCTest
expectation in inverted mode. Read more about it in Unit testing asynchronous Swift code.
为了测试Debouncer
,我们可以使用XCTest
在期待反转的模式。 在单元测试异步Swift代码中了解有关它的更多信息。
To check that a situation does not occur during testing, create an expectation that is fulfilled when the unexpected situation occurs, and set its isInverted property to true. Your test will fail immediately if the inverted expectation is fulfilled.
要检查在测试过程中是否没有发生这种情况,请创建一个在意外情况发生时可以满足的期望,并将其isInverted属性设置为true。 如果满足反向期望,您的测试将立即失败。
class DebouncerTests: XCTestCase { func testDebouncing() { let cancelExpectation = self.expectation(description: "cancel") cancelExpectation.isInverted = true
let completeExpectation = self.expectation(description: "complete") let debouncer = Debouncer(delay: 0.3)
debouncer.schedule { cancelExpectation.fulfill() }
debouncer.schedule { completeExpectation.fulfill() }
wait(for: [cancelExpectation, completeExpectation], timeout: 1) }}
Sometimes small refactoring can have a large effect. A disabled button can lead to unusable screens afterward. UITest helps ensuring integrity and functional aspects of the app. Test should be declarative. We can use the Robot pattern.
有时,小的重构会产生很大的影响。 禁用的按钮之后可能导致屏幕无法使用。 UITest帮助确保应用程序的完整性和功能性。 测试应该是声明性的。 我们可以使用机器人模式 。
class RecipesUITests: XCTestCase { var app: XCUIApplication!
override func setUp() { super.setUp() continueAfterFailure = false
app = XCUIApplication() }
func testScrolling() { app.launch()
let collectionView = app.collectionViews.element(boundBy: 0) collectionView.swipeUp() collectionView.swipeUp() }
func testGoToDetail() { app.launch()
let collectionView = app.collectionViews.element(boundBy: 0) let firstCell = collectionView.cells.element(boundBy: 0) firstCell.tap() }}
Here are some of my articles regarding testing.
这是我有关测试的一些文章。
Running UITests with Facebook login in iOS
在iOS中使用Facebook登录运行UITests
Testing in Swift with Given When Then pattern
使用给定的When Then模式在Swift中进行测试
Accessing the UI from the background queue can lead to potential problems. Earlier, I needed to use MainThreadGuard, now that Xcode 9 has Main Thread Checker, I just enabled that in Xcode.
从后台队列访问UI可能会导致潜在的问题。 之前,我需要使用MainThreadGuard ,因为Xcode 9具有Main Thread Checker ,我只是在Xcode中启用了它。
The Main Thread Checker is a standalone tool for Swift and C languages that detects invalid usage of AppKit, UIKit, and other APIs on a background thread. Updating UI on a thread other than the main thread is a common mistake that can result in missed UI updates, visual defects, data corruptions, and crashes.
Main Thread Checker是用于Swift和C语言的独立工具,它可以检测AppKit,UIKit和其他API在后台线程上的无效使用。 在主线程以外的其他线程上更新UI是一个常见错误,可能会导致错过UI更新,视觉缺陷,数据损坏和崩溃。
We can use Instruments to thoroughly profile the app. For quick measurement, we can head over to the Debug Navigator
tab and see CPU, Memory and Network usage. Check out this cool article to learn more about instruments.
我们可以使用Instruments彻底剖析该应用程序。 为了进行快速测量,我们可以转到Debug Navigator
选项卡,然后查看CPU,内存和网络使用情况。 查看这篇很酷的文章,以了解有关乐器的更多信息。
Playground is the recommended way to prototype and build apps. At WWDC 2018, Apple introduced Create ML which supports Playground to train model. Check out this cool article to learn more about playground driven development in Swift.
建议使用Playground进行原型设计和构建。 在WWDC 2018上,Apple推出了支持Playground训练模型的Create ML 。 查看这篇很酷的文章,以了解有关Swift中游乐场驱动开发的更多信息。
Thanks for making it this far. I hope you’ve learnt something useful. The best way to learn something is to just do it. If you happen to write the same code again and again, make it as a component. If a problem gives you a hard time, write about it. Share your experience with the world, you will learn a lot.
感谢您到目前为止。 希望您学到了一些有用的东西。 学习某事的最好方法就是去做。 如果碰巧一次又一次编写相同的代码,请将其作为组件。 如果问题给您带来困难,请写下来。 与世界分享您的经验,您将学到很多东西。
I recommend checking out the article Best places to learn iOS development to learn more about iOS development.
我建议您查看文章学习iOS开发的最佳场所,以了解有关iOS开发的更多信息。
If you have any questions, comments or feedback, don’t forget to add them in the comments. And if you found this article useful, don’t forget to clap. ?
如果您有任何问题,评论或反馈,请不要忘记在评论中添加它们。 而且,如果您发现本文很有用,请别忘了鼓掌。 ?
If you like this post, consider visiting my other articles and apps ?
如果您喜欢这篇文章,请考虑访问我的其他文章和应用程序 ?
翻译自: https://www.freecodecamp.org/news/learn-ios-best-practices-by-building-a-simple-recipes-app-9bcbce4d10d/
beta应用程序ios