好习惯一
在一个类型里面存储两个不同类型的值
www.swiftwithvincent.com/blog/storin…
自定义一个泛型enum Either存储任意两个不同类型的值
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
// ....
}
struct Advertisement: Identifiable {
let id = UUID()
// ...
}
struct ContentView: View {
@State var content: [Movie]
var body: some View {
List(content) { movie in
// display the `movie`
}
}
}
enum Either<Left, Right> {
case left(Left)
case right(Right)
}
extension Either: Identifiable where Left: Identifiable,
Right: Identifiable,
Left.ID == Right.ID {
var id: Left.ID {
switch self {
case .left(let left):
return left.id
case .right(let right):
return right.id
}
}
}
方便的构建一个反向滚动的列表
www.swiftwithvincent.com/blog/buildi…
通过编写一个上下翻转的ViewModifier,先对列表进行翻转,然后对列表中的每一项进行翻转
这种场景非常适合用来做消息聊天列表
import SwiftUI
struct ChatMessage: View {
let text: String
var body: some View {
HStack {
Text(text)
.foregroundStyle(.white)
.padding()
.background(.blue)
.clipShape(
RoundedRectangle(cornerRadius: 16)
)
.overlay(alignment: .bottomLeading) {
Image(systemName: "arrowtriangle.down.fill")
.font(.title)
.rotationEffect(.degrees(45))
.offset(x: -10, y: 10)
.foregroundStyle(.blue)
}
Spacer()
}
.padding(.horizontal)
}
}
struct BadChatView: View {
@State var data = (0...20).map { $0 }
var body: some View {
ScrollViewReader { scrollView in
List(data.reversed(), id: \.self) { int in
ChatMessage(text: "\(int)")
.listRowSeparator(.hidden)
.id(int)
.onAppear {
if int == data.last {
loadMoreData()
}
}
}
.listStyle(.plain)
.onAppear {
scrollView.scrollTo(data.first!)
}
}
}
func loadMoreData() {
guard data.count < 40 else { return }
let additionalData = (21...40).map { $0 }
data.append(contentsOf: additionalData)
}
}
struct GoodChatView: View {
@State var data = (0...20).map { $0 }
var body: some View {
List(data, id: \.self) { int in
ChatMessage(text: "\(int)")
.flippedUpsideDown()
.listRowSeparator(.hidden)
.onAppear {
if int == data.last {
loadMoreData()
}
}
}
.listStyle(.plain)
.flippedUpsideDown()
}
func loadMoreData() {
guard data.count < 40 else { return }
let additionalData = (21...40).map { $0 }
data.append(contentsOf: additionalData)
}
}
struct FlippedUpsideDown: ViewModifier {
func body(content: Content) -> some View {
content
.rotationEffect(.radians(Double.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}
extension View {
func flippedUpsideDown() -> some View {
modifier(FlippedUpsideDown())
}
}
3个很有用的开发Swift的建议
www.swiftwithvincent.com/blog/here-a…
不要误以为enumerate的第一个参数是下标
我们遍历一个集合的时候经常会使用下面的方式。但是这样获取的i并不是集合的下标,它只是迭代器的下标,虽然它大多数情况下和集合下标相等
var ingredients = ["potatoes", "cheese", "cream"]
for (i, ingredient) in ingredients.enumerated() {
print("ingredient number is \(i + 1) is \(ingredient)")
}
建议使用下面的两种方式准确获取index下标
// 使用zip缝合index和值value
// Array<String>
var ingredients = ["potatoes", "cheese", "cream"]
// Array<String>.SubSequence
var doubleIngredients = ingredients.dropFirst()
for (i, ingredient) in zip(doubleIngredients.indices, doubleIngredients) {
// Correctly use the actual indices of the subquence
doubleIngredients[i] = "\(ingredient) X 2"
}
println(doubleIngredients)
// 使用 indexed方法同时遍历下标和值
// Array<String>
var ingredients = ["potatoes", "cheese", "cream"]
// Array<String>.SubSequence
var doubleIngredients = ingredients.dropFirst()
for (i, ingredient) in doubleIngredients.indexed() {
// Correctly use the actual indices of the subquence
doubleIngredients[i] = "\(ingredient) X 2"
}
println(doubleIngredients)
使用标签跳出多层循环
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
let valueToFind = 5
searchValue: for row in matrix {
for num in row {
if num == valueToFind {
print("Value \(valueToFind) found!")
break searchValue
}
}
}
重写模式匹配运算符 ~= 支持自定义类型进行匹配
struct Circle {
var radius: Double
}
func ~= (pattern: Double, value:Circle) -> Bool {
return value.radius == pattern
}
func ~= (pattern: ClosedRange<Double>, value: Circle) -> Bool {
return pattern.containes(value.radius)
}
let myCircle = Circle(radius:5)
switch myCircle {
case 5:
print("Circle with a radius of 5")
case 1...10:
print("Circle with a radius between 1 and 10")
default:
print("Circle with a different radius")
}
可以使用Xcode15及以上版本预览iOS17的界面
www.swiftwithvincent.com/blog/previe…
import UIKit
@available(iOS 17, *)
#Preview {
let label = UILabel()
label.text = "Hello, UIKit world!"
return label
}
使用正则配合捕获组进行代码重构
www.swiftwithvincent.com/blog/how-to… 原始代码是这样的
先写一个String扩展
使用 NSLocalizedString\((".*"), comment: ""\)
查找,并使用$1.localized
按照分组替换后,可循序重构成如下代码
开启Xcode的自动拼写检查
www.swiftwithvincent.com/blog/how-to…
any代表遵循协议的任意类型,在编译时会生成Container类型。Any代表任意类型,在实际使用时需要as转换成具体类型
www.swiftwithvincent.com/blog/whats-…
// using `any`
protocol NetworkServicing {
func fetchUserName() async -> String
}
class ViewModel {
var userName: String?
private var service: any NetworkServicing
init(service: any NetworkServicing) {
self.service = service
}
func fetchData() {
Task {
userName = await service.fetchUserName()
}
}
}
// using `Any`
let canContainAnything: [Any] = [
"Hello",
42,
URL(string: "https://swift.org")!,
{ (arg: Int) in print(arg) },
]
if let string = canContainAnything.first as? String {
print("It contains the string \(string)")
}
var userInfo: [AnyHashable : Any]?
使用Xcode更方便的测试通知
www.swiftwithvincent.com/blog/how-to…
- 在工程中新建一个 .apns 的文件
2. 文件内容如下
需要将Simulator Target Bundle修改为你App的bundle id
3. 通过将文件拖拽到模拟器 或者执行
xcrun simctl push booted test-notification.apns
来将通知推送到模拟器
当然你也可以使用类似的工具
- SmartPush github.com/shaojiankui…
- ApplePushTesting github.com/dourgulf/Ap…
善于使用Swift中的各种Formatters进行格式化展示
www.swiftwithvincent.com/blog/swift-…
// #01 - DateComponentsFormatter
import Foundation
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = true
formatter.includesTimeRemainingPhrase = true
formatter.allowedUnits = [.minute]
// Result: "About 5 minutes remaining"
let outputString = formatter.string(from: 300.0)
// #02 - DateIntervalFormatter
import Foundation
let formatter = DateIntervalFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
// Create two dates that are exactly 1 day apart.
let startDate = Date()
let endDate = Date(timeInterval: 86400, since: startDate)
// Result: "03/03/2024 – 04/03/2024”
let outputString = formatter.string(from: startDate, to: endDate))
// #03 - MeasurementFormatter
import Foundation
let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "en_US")
let measurement = Measurement(
value: 1.2,
unit: UnitLength.kilometers
)
// Result: "0.746 mi"
let outputString = formatter.string(from: measurement)
// #04 - ByteCountFormatter
import Foundation
let formatter = ByteCountFormatter()
formatter.countStyle = .file
// Result: "1 GB"
let outputString = formatter.string(fromByteCount: 1_000_000_000)
// #05 - PersonNameComponentsFormatter
import Foundation
let formatter = PersonNameComponentsFormatter()
var components = PersonNameComponents()
components.givenName = "Vincent"
components.familyName = "Pradeilles"
// Result: "Vincent Pradeilles"
let firstOutputString = formatter.string(from: components)
formatter.locale = Locale(identifier: "ko_KR")
// Result: "Pradeilles Vincent"
let secondOutputString = formatter.string(from: components)
使用Swift宏生成EnvironmentKey和对应的默认值
www.swiftwithvincent.com/blog/using-…
依赖package:github.com/Wouter01/Sw…
我们定义一个EnvironmentKey的时候,需要定义一个自定义的struct,扩展EnvironmentValue写对应的getter和setter。有了上面的宏之后这些繁复的代码就可以在编译期自动完成
// Without the macro
import SwiftUI
struct UserNameEnvironmentKey: EnvironmentKey {
static var defaultValue: String = "Anonymous"
}
extension EnvironmentValues {
var userName: String {
get {
self[UserNameEnvironmentKey.self]
}
set {
self[UserNameEnvironmentKey.self] = newValue
}
}
}
// With the macro
import SwiftUI
import SwiftUIMacros
@EnvironmentStorage
extension EnvironmentValues {
var userName: String = "Anonymous"
}
做动画时,调用View的layoutIfNeeded让约束立即生效
www.swiftwithvincent.com/blog/do-you…
在我们使用AutoLayout时,如果在动画blok里面改变了约束,约束不会立即生效,这时就需要添加layoutIfNeeded立即计算约束
// First Example
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var blueView: UIView!
@IBOutlet weak var blueViewHeightConstraint: NSLayoutConstraint!
@IBAction func increaseHeightButtonTapped(_ sender: Any) {
print(blueView.frame.height) // 240
blueViewHeightConstraint.constant += 50
print(blueView.frame.height) // 240
view.layoutIfNeeded()
print(blueView.frame.height) // 290
}
}
// Second Example
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var blueView: UIView!
@IBOutlet weak var blueViewHeightConstraint: NSLayoutConstraint!
@IBAction func increaseHeightButtonTapped(_ sender: Any) {
UIView.animate(withDuration: 0.3) {
self.blueViewHeightConstraint.constant += 50
self.view.layoutIfNeeded()
}
}
}
onTapGesture好用,可不要贪杯哦
www.swiftwithvincent.com/blog/be-car…
- onTapGesture可以给任意View添加点击事件,但是注意注意不要乱用
- 在明确需要用户操作的地方,还是使用Button吧,它不仅能提供视觉反馈,还有附带的障碍功能提示
// First Use Case
import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
// ...
}
.onTapGesture {
viewModel.prefetchData()
}
}
}
// Second Use Case (Bad)
import SwiftUI
struct ContentView: View {
@State var showModal = false
var body: some View {
VStack {
// ...
}
.sheet(isPresented: $showModal){
// ...
}
.onTapGesture {
showModal = true
}
}
}
// Second Use Case (Good)
import SwiftUI
struct ContentView: View {
@State var showModal = false
var body: some View {
Button {
showModal = true
} label: {
VStack {
// ...
}
}
.sheet(isPresented: $showModal){
// ...
}
}
}
使用Xcode功能简单的模拟应用内购买
www.swiftwithvincent.com/blog/how-to…
假设有如下的代码,我们需要显示三款产品并提供点击购买的功能
如果我们运行后,会发现什么都没有展示。这是因为我们没有配置文件
创建StoreKit Configuration File
在菜单,新建文件里选择如下
填写配置文件名称,后面会用到。如果已经上架过购买功能,最好把红框内的内容也勾选上
选择一种类型
填写名称和产品ID,产品ID是我们上面需要展示的ID
填写产品的显示名称和价格
运行App的时候配置一下Scheme
运行起来之后就可以测试购买流程了
使用URLProtocol无侵入性的mock任意一个网络请求
www.swiftwithvincent.com/blog/how-to…
// UserAPI.swift
import Foundation
// 定义一个User Model类
struct User: Decodable {
let firstName: String
let lastName: String
}
// 网络请求类
final class UserAPI {
let endpoint = URL(string: "https://my-api.com")!
let session: URLSession
let decoder: JSONDecoder
init(
session: URLSession = URLSession.shared,
decoder: JSONDecoder = JSONDecoder()
) {
self.session = session
self.decoder = decoder
}
func fetchUser() async throws -> User {
return try await request(url: endpoint.appendingPathComponent("user/me"))
}
// 使用Session进行网络请求
private func request<T>(url: URL) async throws -> T where T: Decodable {
let (data, _) = try await session.data(from: url)
return try decoder.decode(T.self, from: data)
}
}
// MockURLProtocol.swift
import XCTest
// 定义一个用于Mock的URLProtocol
class MockURLProtocol: URLProtocol {
// 重写一些方法
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
XCTFail("No request handler provided.")
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
XCTFail("Error handling the request: \(error)")
}
}
override func stopLoading() {}
}
// UserAPITests.swift
import Foundation
import XCTest
@testable import MyApp
class UserAPITests: XCTestCase {
lazy var session: URLSession = {
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
return URLSession(configuration: configuration)
}()
lazy var api: UserAPI = {
UserAPI(session: session)
}()
override func tearDown() {
MockURLProtocol.requestHandler = nil
super.tearDown()
}
func testFetchUser() async throws{
let mockData = """
{
"firstName": "Vincent",
"lastName": "Pradeilles"
}
""".data(using: .utf8)!
MockURLProtocol.requestHandler = { request in
XCTAssertEqual(request.url?.absoluteString, "https://my-api.com/user/me")
let response = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
return (response, mockData)
}
let result = try await api.fetchUser()
XCTAssertEqual(result.firstName, "Vincent")
XCTAssertEqual(result.lastName, “Pradeilles")
}
}
多使用 .isMultiple(of:) 进行条件判断,让意图更加明显
www.swiftwithvincent.com/blog/bad-pr…
// Before
import Foundation
let myInteger = Int.random(in: 0...10)
if myInteger % 2 == 0 {
print("it's even")
} else {
print("it's odd")
}
// After
import Foundation
let myInteger = Int.random(in: 0...10)
// 代码看起来更加清晰,意图也变得更加明显
if myInteger.isMultiple(of: 2) {
print("it's even")
} else {
print("it's odd")
}
使用锁、钥匙 模式写更安全的代码
www.swiftwithvincent.com/blog/how-to…
有瑕疵的代码
- 下面的代码 fetchNewsFeed 总是假定已经有token,但其实没有人保证这个
- 代码假设方法authenticate()将始终在方法fetchNewsFeed()之前调用
// 网络客户端
actor NetworkClient {
var authToken: String?
// 获取身份验证令牌
func authenticate(
login: String,
password: String
) async {
// ...
self.authToken = authToken
}
// 使用身份验证令牌来检索一些实际数据
func fetchNewsFeed() async throws -> NewsFeed {
guard let authToken else {
throw NetworkError.noToken
}
// ...
}
}
完善的代码
希望有一种机制,只要属性authToken仍然设置为nil,就不可能调用fetchNewsFeed()方法
-
将NetworkClient拆成两部分:UnauthenticatedNetworkClient 和AuthenticatedNetworkClient。
-
在AuthenticatedNetworkClient中,我们将authToken设置为非可选类型
- 这样为了初始化AuthenticatedNetworkClient,现在必须提供authToken
-
更新UnauthenticatedNetworkClient,使方法authenticate()返回一个AuthenticatedNetworkClient
actor UnauthenticatedNetworkClient {
func authenticate(
login: String,
password: String
) async -> AuthenticatedNetworkClient {
// ...
return AuthenticatedNetworkClient(authToken: authToken)
}
}
actor AuthenticatedNetworkClient {
var authToken: String
init(authToken: String) {
self.authToken = authToken
}
func fetchNewsFeed() async throws -> NewsFeed {
// ...
}
}
使用 String的 compare方法比较字符串
www.swiftwithvincent.com/blog/bad-pr…
- 每次调用.lowercased()方法时,它都会创建一个新的字符串副本,这可能会对性能产生负面影响
import Foundation
let searchQuery = "cafe"
let databaseValue = "Café"
let comparisonResult = searchQuery.compare(
databaseValue,
// 大小写不敏感、重音符号不敏感
options: [.caseInsensitive, .diacriticInsensitive]
)
if comparisonResult == .orderedSame {
// ...
}
理解 translatesAutoresizingMaskIntoConstraints
www.swiftwithvincent.com/blog/do-you…
Masonry或者SnapKit都是使用的AutoLayout技术,我们在使用Masonry或者SnapKit的时候经常需要将View的translatesAutoresizingMaskIntoConstraints
设置为false
这里我们来了解一下translatesAutoresizingMaskIntoConstraints
官方文档说明
-
它主要用于控制是否将View的autoresizing mask转换成Auto Layout的约束
-
如果将这个属性的值设置为true,系统会创建一组约束,以复制视图的
autoresizing mask
所指定的行为- 也就是说:当将该属性设置为true时,除了我们自己添加的约束之外,系统还会自动添加一组约束到视图中
-
autoresizing mask constraints
完全指定了视图的大小和位置;如果要添加额外的约束来修改这个大小或位置,就会引入冲突。如果要使用AutoLayout就必须设置为false并提供约束 -
默认情况下,对于通过编程方式创建的任何视图,该属性被设置为true。如果在Interface Builder中添加视图,系统会自动将该属性设置为false
- 也就是说:在Interface Builder中创建的视图(即.xib或.storyboard文件中)默认情况下该属性设置为false。在代码中创建的视图默认情况下该属性设置为true
autoresizing masks
autoresizing mask
提供了一个简单的API,用于描述子视图在其父视图大小更改时应如何响应。
换句话说,视图的autoresizing mask
允许您指定当其父视图调整大小时,视图的高度、宽度、顶部、左侧、右侧和底部边距是否应该改变
可以在Xcode中调整autoresizing mask
几个问题
-
translatesAutoresizingMaskIntoConstraints实际上是做什么的?
- translatesAutoresizingMaskIntoConstraints是UIView的一个属性,它使开发人员能够将iOS最初使用的布局系统模拟为更近期的AutoLayout系统。
- 为了不破坏在AutoLayout之前编写的UI代码,iOS决定在以编程方式初始化UIView时将该属性设置为true。
- 当我们想要为UIView添加我们自己的一组AutoLayout约束时,非常重要的一点是我们必须手动将该属性设置回false,否则两组约束将会发生冲突
巧用defer处理必走的回调
www.swiftwithvincent.com/blog/how-to…
- 如下Before部分的代码,如果我们少写了一个completion(xxx),代码不会报错,但实际上逻辑并不完备,有潜在的风险
- 而After部分的代码,完美的解决了这个问题。编译器会保证result一定有值并且只会被赋值一次,defer会在fetchData返回前最后执行一次,此时result一定是有值的
// Before
import Foundation
func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
guard error == nil else {
completion(.failure(error!))
return
}
guard let data else {
completion(.failure(NetworkError.noData))
return
}
completion(.success(data))
}
.resume()
}
// After
import Foundation
func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
let result: Result<Data, Error>
defer {
completion(result)
}
guard error == nil else {
result = .failure(error!)
return
}
guard let data else {
result = .failure(NetworkError.noData)
return
}
result = .success(data)
}
.resume()
}
多使用prepareForDisplay加载大的图片文件
www.swiftwithvincent.com/blog/bad-pr…
prepareForDisplay 会在后台线程解码图片,并且解码完成准备显示时回调completionHandler 从而提升性能
import UIKit
// Before
// 直接加载大图会阻塞主线程
let image = UIImage(named: "big-image")
imageView.image = image
// After
let image = UIImage(named: "big-image")
// 使用 UIKit中回调的方式 异步加载大图
image?.prepareForDisplay { [weak self] preparedImage in
DispatchQueue.main.async {
self?.imageView.image = preparedImage
}
}
// using Swift Concurrency
let image = UIImage(named: "big-image")
// 如果使用并发异步进行加载图片,加载完成后会自动切换回
Task {
imageView.image = await image?.byPreparingForDisplay()
}
下标语法支持重载,可以用它来重写一个带有safe标签 的下标
www.swiftwithvincent.com/blog/hidden… subscript 和 方法 除了声明的关键字以及subscript没有名字外非常相似
// 声明下标语法
extension Array {
subscript(index: Int) -> Element {
// ...
}
}
// 下标语法通过中括号调用
let numbers = [0, 2, 4]
numbers[2] // returns 4
// 下标语法可以有不止一个参数
extension Dictionary {
subscript(
key: Key,
default defaultValue: @autoclosure () -> Value
) -> Value {
// ...
}
}
let dictionary = [ "zero": 0, "one": 1, "two": 2]
dictionary["two", default: -1] // returns 2
dictionary["three", default: -1] // returns -1
// subscript默认是没有参数标签的,我们可以添加参数标签进行重载制作一个safe方法
extension Collection {
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
let data = [1, 3, 4]
data[10] // will crash 💥
data[safe: 10] // returns nil
在单元测试里也可以简单测试程序性能
www.swiftwithvincent.com/blog/how-to…
- 写一个测试函数,使用measure函数的闭包参数将我们的代码放进去
- 第一次运行时会取10次的平均值,我们可以把这个值设置为基准
3. 之后,如果该测试函数的耗时超过基准,这个单元测试就会失败
// App Code
import Foundation
func costlyFunction() -> Int {
var result = 0
for i in 1...2_000_000 {
result += i
}
return result
}
// Test Code
import XCTest
@testable import TestPerformance
final class TestPerformanceTests: XCTestCase {
func testPerformance() throws {
measure(metrics: [XCTMemoryMetric(), XCTStorageMetric()]) {
let _ = costlyFunction()
}
}
}
格式化日期使用yyyy,不要使用YYYY
www.swiftwithvincent.com/blog/bad-pr…
依稀记得前些年刷博客刷到一个使用YYYY格式化日期导致的问题。 blog.csdn.net/shenzhou_yh…
YYYY如果最后一周是跨年的,会算作上一年从而导致错误的产生
import Foundation
let date = dateOf_01_01_2023()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/YYYY"
dateFormatter.string(from: date) // 01/01/2022
dateFormatter.dateFormat = "dd/MM/yyyy"
dateFormatter.string(from: date) // 01/01/2023
5.9之后,多使用Switch和if 表达式简化代码
www.swiftwithvincent.com/blog/hidden…
从swift5.9开始,switch和if的简单语句也可以被当做表达式来使用
复杂代码
// 先声明变量及其类型,然后在每个case中给变量赋值
let comment: String
if Int.random(in: 0...3).isMultiple(of: 2) {
comment = "It's an even integer"
} else {
comment = "It's an odd integer"
}
let spelledOut: String
switch Int.random(in: 0...3) {
case 0: spelledOut = "Zero"
case 1: spelledOut = "One"
case 2: spelledOut = "Two"
case 3: spelledOut = "Three"
default: spelledOut = "Out of range"
}
简化一点代码
// 使用闭包可以省略先声明变量及其类型这一步骤,从而让编译器自动推断,然后在每个case中给变量赋值
let comment = {
if Int.random(in: 0...3).isMultiple(of: 2) {
return "It's an even integer"
} else {
return "It's an odd integer"
}
}()
let spelledOut = {
switch Int.random(in: 0...3) {
case 0: return "Zero"
case 1: return "One"
case 2: return "Two"
case 3: return "Three"
default: return "Out of range"
}
}()
最简代码
import Foundation
let comment = if Int.random(in: 0...3).isMultiple(of: 2) {
"It's an even integer"
} else {
"It's an odd integer"
}
let spelledOut = switch Int.random(in: 0...3) {
case 0:
"Zero"
case 1:
"One"
case 2:
"Two"
case 3:
"Three"
default:
"Out of range"
}
利用PhantomType隔离相同成员的不同类型,让问题在编译期暴露出来
www.swiftwithvincent.com/blog/bad-pr…
下面的代码,handle函数中对locations调用filter,Person和Location都有一个id字段且类型相同,为了在filter中Person和Location的id不可比较,我们使用了PhantomType类型 Id<T> 作为它们各自的id类型
import Foundation
struct ID<T>: Equatable {
private let value = UUID()
}
struct Person {
let id = ID<Self>() // `id` is of type `ID<Person>`
let name: String
}
struct Location {
let id = ID<Self>() // `id` is of type `ID<Location>`
let coordinates: (Double, Double)
}
func handle(locations: [Location]) {
let me = Person(name: "Vincent")
let filtered = locations.filter { $0.id == me.id }
}
定义常量时考虑使用enum 而不是将它们定义在全局命名空间中
www.swiftwithvincent.com/blog/hidden…
错误实践
一般情况下,我们定义常量时会在全局命名空间或者局部变量中
import Foundation
let baseUrl = URL(string: "https://myapi.com")
let apiPath = "/api/v2/"
let apiKey = "fskf8h99Fs7HV1jHNJF19g0268"
正确实践
将定义放到没有case 变量的enum里面,enum没有构造函数不会被外部创建实例,可以保证所有的变量仅通过enum 名访问
import Foundation
enum Constant {
static let baseUrl = URL(string: "https://myapi.com")
static let apiPath = "/api/v2/"
static let apiKey = "fskf8h99Fs7HV1jHNJF19g0268"
}
多行字符串时多使用"""语法,而不是穿插\n
www.swiftwithvincent.com/blog/bad-pr…
let string = """
1st line
2nd line
3rd line
4th line
"""
当你确定一个类不被继承或一个方法不被重载时,尝试使用final修饰它,从而享受编译器带来的优化
www.swiftwithvincent.com/blog/hidden…
下面的代码使用final修饰后
- 对firstMethod和secondMethod的调用将从动态调用转变为静态调用
- 提高了代码可读性。其他人读到该代码时就已经确定此为最终实现,无需一步步往上过一遍父类实现
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ...
}
func firstMethod() {
// ...
}
func secondMethod() {
// ...
}
}
iOS14之后,请使用UIAction替代OC Style的#selector(methodName)
www.swiftwithvincent.com/blog/bad-pr…
当我们给Button添加事件的时候,会习惯性的使用OC风格的#selector,但其实有更好的方式。 使用OC风格的#selector,有以下几个问题
- 性能略差。OC的Method走的消息发送那一套
- 需要属性标注.@objc,#selector(methodName)
- .touchUpInside仅限于Button这类触摸屏上的控件
修改为Swift风格之后,有以下好处
- 性能会稍微好一些。Swift类中的方法默认走的类似虚表那一套
- .primaryActionTriggered甚至可以响应Apple TV上使用遥控器选中、iPad上使用键盘选中等方式。稍微有了些跨平台性
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let action = UIAction { [weak self] _ in
self?.buttonTapped()
}
button.addAction(action, for: .primaryActionTriggered)
}
func buttonTapped() {
print("Button was tapped!")
}
}
把可失败的初始化处理交给调用者
www.swiftwithvincent.com/blog/hidden…
不错的代码
假如我们有一个User类,可以通过name参数初始化,但又不希望name为空。这时候我们会选择使用可失败的构造器init?
但这样有一个问题,调用者没法知道这个User是可失败的,也就不会处理可能失败的case。
// Failable init
struct User {
let name: String
init?(name: String) {
guard name.isEmpty == false else {
return nil
}
self.name = name
}
}
let user = User(name: "") // nil
更好的代码
为了让调用者在调用的时候就知道可失败,并且处理失败的原因,我们可以使用如下的代码
// Throwing init
enum UserCreationError: Error {
case emptyName
}
struct User {
let name: String
init(name: String) throws {
guard name.isEmpty == false else {
throw UserCreationError.emptyName
}
self.name = name
}
}
do {
let user = try User(name: "")
} catch {
print(error) // emptyName
}
使用 泛型语法配合typealias定义别名,让代码更加通用
www.swiftwithvincent.com/blog/hidden…
typealias Point<T: Numeric> = (x: T, y: T)
let pointUsingInt = Point(x: 1, y: 2) ✅
let pointUsingDouble = Point(x: 3.0, y: 4.0) ✅
let pointUsingString = Point(x: "a", y: "b") ❌
typealias CompletionHandler<T> = (Result<T, Error>) -> Void
func fetchData(_ completion: @escaping CompletionHandler<Data>) {
// ...
}
使用系统API PersonNameComponentsFormatter 让代码处理更加本地化
www.swiftwithvincent.com/blog/bad-pr…
import Foundation
struct User {
let givenName: String
let familyName: String
}
let personNameFormatter = PersonNameComponentsFormatter()
extension User {
var fullName: String {
var components = PersonNameComponents()
components.givenName = givenName
components.familyName = familyName
return personNameFormatter.string(from: components)
}
}
let user = User(
givenName: "Vincent",
familyName: "Pradeilles"
)
user.fullName // Vincent Pradeilles
let koreanLocale = Locale(identifier: "ko_KR")
personNameFormatter.locale = koreanLocale
user.fullName // Pradeilles Vincent