介绍Introducing

iPerfman-iPhone-screenshot

iPerfman-iPad-screenshot

iPerfman-mac-screenshot

iPerfman-tv-screenshot

iPerfman 是一个 iPerf3工具,支持所有苹果平台:iOS、iPadOS、macOS和tvOS。

iPerf 系列工具执行测量 IP 网络上可实现的最大带宽。

具有漂亮速度仪和速度图表。

您可以测试局域网网络速度以找到最佳的WIFI路由器位置。

您可以通过手动设置测试参数来测试更多复杂的网络,例如可以测试 UDP 模式的性能,并使用带宽限制功能来模拟具有带宽限制的网络环境。

iPerfman 支持中文和英文两种语言,您可以在设置中选择您喜欢的语言,便于使用。

总之,iPerfman 是一款强大的网络测速工具,可以帮助您评估网络性能,为您提供更优质的网络体验。

iPerfman is an iPerf3 tool that supports all apple platforms: iOS, iPadOS, macOS, and tvOS.

The iperf series of tools perform active measurements to determine the maximum achievable bandwidth on IP networks.

Beautiful speedometer and speed chart.

You can test the LAN network speed to find the best WIFI router location.

You can test more complex networks by manually setting test parameters, for example to test the performance of UDP mode, and use the bandwidth-limiting functionality to simulate a bandwidth-constrained network environment.

iPerfman supports both Chinese and English, and you can choose your preferred language in the settings for ease of use.

Overall, iPerfman is a powerful network speed measurement tool that helps you evaluate network performance and provides you with a better network experience.

App Store下载
Download on the App Store

有任何疑问或者建议,可以通过 Email 联系。

If you have any questions or suggestions, you can contact them through Email.

隐私政策Privacy policy

本App不进行任何隐私信息收集或上传。

This App does not collect or upload any private information.

UserDefaults 和 AppStorage 混用时,默认值不统一的问题。因为AppStorage持久化时机不确定,UserDefaults先调用时可能获取的值仍是系统默认值。

解决方案有两种:

1. UserDefaults.standard.object(forKey:) == nil 判断。

1
2
3
if UserDefaults.standard.object(forKey: "hasHapticFeedback") == nil {
UserDefaults.standard.set(true, forKey: "hasHapticFeedback")
}

2. 借助SwiftyUserDefaults,但是对于纯SwiftUI项目,没必要。

1
2
3
4
5
extension DefaultsKeys {
var userColorScheme: DefaultsKey<String> { .init("userColorScheme", defaultValue: "default") }
var userThemeName: DefaultsKey<String?> { .init("userThemeName") }
var userLastLoginDate: DefaultsKey<Date?> { .init("userLastLoginDate") }
}

水平布局时,滚动正常,回车键也能正常退出编辑。底层是UITextField
垂直布局时,底层是UITextView,表现不一致,出现问题。

  1. 焦点问题需要我们滚动时,锚点设为顶部,proxy.scrollTo($0, anchor: .top)
  2. 回车键,退出编辑失效的问题,需要借助toolbar

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import SwiftUI

struct TestNextTextFieldFocus: View {
@State var inputsValues: [String] = (0..<30).map { i in
"test \(i) TextField axis is \(i % 2 == 0 ? ".vertical" : ".horizontal")"
}

@FocusState var focusedInput: Int?

var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0..<inputsValues.count, id: \.self) { i in
TextField("Value", text: $inputsValues[i], axis: i % 2 == 0 ? .vertical : .horizontal)
.focused($focusedInput, equals: i)
.submitLabel(.return)
.id(i)
.onSubmit {
print("submit \(i)")
// update state here !!
if (i + 1) < inputsValues.count {
focusedInput = i + 1
} else {
focusedInput = nil
}
}
}
}
.onChange(of: focusedInput) {
print("onChange focusedInput:\(String(describing: focusedInput))")
// react on state change here !!
proxy.scrollTo($0, anchor: .top)
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button {
focusedInput = nil
} label: {
Text("Done")
}
}
}
}
}
}

struct TestNextTextFieldFocus_Previews: PreviewProvider {
static var previews: some View {
TestNextTextFieldFocus()
}
}

介绍Introducing

Mountpaper-iPhone-screenshot1
Mountpaper-iPad-screenshot1
Mountpaper-mac-screenshot1

裱纸是装裱壁纸的意思。提供许多高质量壁纸。

Mountpaper is meaning of mount wallpaper. Offer a lot of high quality wallpapers.

下载地址 Download on:App Store

有任何疑问或者建议,可以通过 Email 联系。

If you have any questions or suggestions, you can contact them through Email.

隐私政策Privacy policy

本App不进行任何隐私信息收集或上传。

This App does not collect or upload any private information.

SwiftyModel-mac-preview

介绍Introducing

使用 SwiftyJSON 生成在 Swift 语言中支持 SwiftyJSON 的 Model(模型)。

一键复制和分享代码。

Use SwiftyJSON to generate models that support SwiftyJSON in Swift Language.

One-click to copy or share code.

例如输入以下JSON:

For example, enter the following JSON:

1
2
3
4
5
6
7
8
9
10
{
"id" : 1,
"name" : "Jacky",
"notifications" : false,
"pets" : [
"cat",
"dog"
]
}

输出Swift Model:

Output Swift Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation
import SwiftyJSON

struct NewModel {
let id: Int
let name: String
let notifications: Bool
let pets: [String]

init(json: JSON) {
id = json["id"].intValue
name = json["name"].stringValue
notifications = json["notifications"].boolValue
pets = json["pets"].arrayValue.map { $0.stringValue }
}
}

下载地址:App Store

App Store下载
Download on the App Store

有任何疑问或者建议,可以通过 Email 联系。

If you have any questions or suggestions, you can contact them through Email.

隐私政策Privacy policy

本App不进行任何隐私信息收集或上传。

This App does not collect or upload any private information.

subscriptions-mac-preview

介绍

  1. 轻松记录全部订阅账单。

  2. 统计平均每日、每周、每月和每年花费。

  3. 支持iCloud同步全设备。

下载地址:App Store

有任何疑问或者建议,可以通过 Email 联系。

隐私政策

本隐私政策描述了我们如何收集,使用和披露与服务有关的个人信息,您通过访问服务即表示同意。“个人信息”是指有关可识别个人的信息,但不包括商业信息。

收集哪些信息

阅账希望使您与我们的体验令人满意且安全。我们的数据收集政策使您可以选择向我们提供多少个人信息,并控制我们如何使用这些信息。我们的目标是为您提供满意的体验,同时让您控制自己的隐私。阅账收集和使用信息主要是为了使您使用我们的服务更轻松,更有意义。如果您选择向我们注册,则可能会在不同时间要求您提供信息,我们将竭尽所能使阅账满足您的服务并只为您提供所需的内容。

收集的信息与用途

为识别您的设备 ID 并预防恶意程序、提高服务安全性、保障运营质量及效率,我们使用的第三方平台包括Firebase 会收集您的设备信息(包括IMEI、MEID、IMSI、GUID、MAC地址)、您安装的应用信息或运行中的进程信息。

匿名信息是指无法与特定个人联系在一起的信息,我们不知道您的姓名,住处或出生日期。匿名信息可能以多种方式收集,包括通过使用 Cookie,网络信标或从您使用的设备中收集。

第三方分析工具

我们的服务可能包含来自我们服务提供商的第三方跟踪以及数据收集和分析工具,例如 Google Analytics(分析)和 Google Firebase 。此类第三方可以在我们的服务中使用 Cookie,API 和 SDK,以使他们能够代表我们收集和分析与用户和设备相关的数据和信息。第三方可能会访问并收集有关您的数据和信息,例如您的设备标识符(包括IMEI、MEID、IMSI、GUID、MAC地址及其他相关信息),语言环境(使用特定语言的特定位置),地理位置信息,IP 地址,应用程序使用情况,访问权限和会话时间,传感器数据,设备上存在的或在设备上特定时间使用的应用程序以及您对广告的观看和互动,以提供其服务,包括例如启用,提供和投放广告在下面有更详细的说明。

我们的服务提供商的隐私政策可能包括有关其数据收集和使用惯例以及跟踪技术的其他条款和披露,我们鼓励您检查这些隐私政策以了解有关其数据收集和使用惯例以及 Cookie 的使用的更多信息以及其他类似的跟踪技术。

为识别您的设备 ID 并预防恶意程序、提高服务安全性、保障运营质量及效率,我们使用的第三方平台将获取设备序列号。

第三方广告合作伙伴

我们与各种广告商,广告网络,广告服务器和分析公司合作。这些广告商,广告网络,广告服务器和分析公司使用各种技术以及第三方公司的技术来收集数据,以便向您和其他用户发送(或投放)相关广告。这些技术可能包括放置Cookie或网络信标,使用唯一或非唯一的非个人标识符,或在我们的服务上使用其他技术,并且这些技术可用于跟踪用户行为,跟踪我们如何服务正在使用中,有可能为您提供更多相关的广告。这些目标广告可能会出现在我们的服务或您访问的其他服务上。本隐私政策不涵盖广告商,广告网络,广告服务器和分析公司对各种技术的使用。这些公司还可能从您从其他公司使用的服务中获取信息,这些服务包括但不限于其他网站,移动网站,可移动下载的应用程序和可下载的桌面应用程序,并将这些信息与他们通过我们的服务通过这些第三方技术获取的信息结合在一起。您应该意识到,我们无法控制这些第三方技术或其中包含的信息。

更新

阅账可以随时修改本隐私政策。请经常检查是否有任何更改。继续使用服务,即表示您接受本隐私政策的所有更新。此版本于 2023 年 02 月 01 日更新。

联系我们

我们希望此声明能为您提供丰富而清晰的信息。如果您有任何疑问或进一步的信息,请给我们发送电子邮件。

App 与服务反馈: 531sunlight@gmail.com

机器配置

主板:微星 MSI MPG Z390 GAMING PRO CARBON AC

CPU:Intel i9 9900K

内存:科赋 DDR4 3200 16G*4

显卡:华硕 ASUS AREZ-STRIX-RXVEGA64-O8G-GAMING

电源:EVGA SuperNOVA 750 G+

散热:MASTERLIQUID ML240R RGB

固态:三星 970 EVO 500G

板载声卡:ALC S1220A

板载有线网卡:Intel I219V7

无线网卡和蓝牙:BCM94360CD

显示器:戴尔 P2715Q 4K显示器

机箱:追风者 PK-515ETG

具体步骤

  1. 替换Clover为Open Core v0.6.2,感谢 pOH7/hackintosh-z390-h-9700kuzh 的EFI。
  2. 下载并升级Big Sur,重启几次后,无法进入Big Sur的安装界面。
  3. 升级Open Core v0.6.3,感谢 Ryzentosh Big Sur Updating OpenCore From 0.6.2 to 0.6.3
    1. 打开OpenCore Sanity Checker ,拖进config.plist,根据建议修改。
    2. 下载OpenCorePkgWhateverGreenVirtualSMCLilu 并替换。其中OpenCore只需要替换EFI/OC/OpenCore.efiEFI/BOOT/BOOTx64.efi
  4. 顺利进入Big Sur的安装界面,升级成功🎉
  5. EFI下载地址: [Big Sur Open Core 0.6.3 EFI 20201114.zip](https://github.com/gewill/Hackintosh-Installation-with-MSI-Z390-and-i9-9900K/blob/master/Big Sur Open Core 0.6.3 EFI 20201114.zip)

Hackintosh-Big-Sur

已知问题

  1. USB 2.0 不可用,需USB定制。
  2. 硬件加速不可用,BIOS集成显卡打开即可硬件加速。

Hardware-Acceleration-On-Big-Sur

Array.swift

Using a negative number or an index equal to or greater than count triggers a runtime error.

使用负数或大于等于count的索引会触发运行时错误。

1
print(emptyDoubles[0])

ClosedRange.swift

An equivalent range must be representable as a closed range. For example, passing an empty range as other triggers a runtime error, because an empty range cannot be represented by a closed range instance.

等效范围必须可以表示为闭合范围。例如,将空范围作为 other 传递会触发运行时错误,因为空范围不能由封闭范围实例表示。

1
2
let emptyRange = 0..<0
ClosedRange(emptyRange)

Dictionary.swift

Passing a sequence with duplicate keys to this initializer results in a runtime error. If your sequence might have duplicate keys, use the Dictionary(_:uniquingKeysWith:) initializer instead.

向此构造器传递具有重复键的序列会导致运行时错误。如果序列可能有重复的键,则使用Dictionary(_:uniquingKeysWith:) 初始化。

1
2
3
let digitWords = ["one", "two", "three", "four", "five"]
let wordToValue = Dictionary(uniqueKeysWithValues: zip(digitWords, 1 ... 5))
let wordToValueAndRemoveDuplicateKeys = Dictionary(zip(["one", "one", "three", "four", "five"], 1 ... 5)) { max($0, $1) }

DictionaryCasting.swift

1
2
var dic: [String: Any] = ["name": "li", "age": 18]
dic as! [String: String]

Integers.swift

When the additive inverse of a value is unrepresentable in a conforming type, the operation should either trap or return an exceptional value. For example, using the negation operator (prefix -) with Int.min results in a runtime error.

当一个值的负值在一个符合类型中不可表示时,操作应该捕获或返回一个异常值。例如,将否定运算符(前缀-)与 Int.min一起使用会导致运行时错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
let x = Int.min
let y = -x
// Overflow error

let z = -Int8.min
// Overflow error

var y = Int8.min
y.negate()
// Overflow error

let z = UInt32(-500)
// Error

Optional.swift

In debug builds (-Onone), the unsafelyUnwrapped property has the same behavior as using the postfix ! operator and triggers a runtime error if the instance is nil.

在调试版本(-Onone)中,unsafelyUnwrapped属性的行为与使用后缀的行为相同!如果实例为 nil ,则触发运行时错误。谨慎使用

1
2
3
let optionalInt: Int? = nil
optionalInt.unsafelyUnwrapped
optionalInt!

Policy.swift

If you have prior knowledge that an AnyObject instance has a particular type, you can use the unconditional downcast operator (as!). Performing an invalid cast triggers a runtime error.

如果您事先知道AnyObject实例具有特定的类型,则可以使用无条件向下转换运算符(as)。 执行无效强制转换会触发运行时错误。

1
2
3
4
5
let message = s as! String
print("Successful cast to String: \(message)")
// Prints "Successful cast to String: This is a bridged string."
let badCase = v as! String
// Runtime error

Range.swift

An equivalent range must be representable as an instance of Range. For example, passing a closed range with an upper bound of Int.max triggers a runtime error, because the resulting half-open range would require an upper bound of Int.max + 1, which is not representable as

范围边界值越界会触发运行时错误,如设置最大值为Int.max时。

1
Range(0 ... Int.max)

Always use the slice’s startIndex property instead of assuming that its indices start at a particular value. Attempting to access an element by using an index outside the bounds of the slice’s indices may result in a runtime error, even if that index is valid for the original collection.

始终使用 slice 的startIndex属性,而不是假设它的索引以特定的值开始。试图通过在slice索引的边界之外使用索引来访问元素可能会导致运行时错误,即使该索引对原始集合有效。

1
2
3
4
5
6
7
8
9
10
11
let streets = ["Adams", "Bryant", "Channing", "Douglas", "Evarts"]
let streetsSlice = streets[2...]
print(streetsSlice)
// ["Channing", "Douglas", "Evarts"]

print(streetsSlice.startIndex)
// 2
print(streetsSlice[2])
// "Channing"
print(streetsSlice[0])
// error: Index out of bounds

RangeReplaceableCollection.swift

Attempting to remove more elements than exist in the collection triggers a runtime error.

尝试移除集合中存在的元素数目多于存在的元素会触发运行时错误。

1
2
3
var array = Array(0 ..< 5)
array.removeFirst(6)
array.removeLast(6)

SetCasting.swift

We can’t just delegate to _setDownCastConditional here because we rely on as! to generate nice runtime errors when the downcast fails.

Set 向下强制转换失败时,会产生运行时错误。

1
2
var s: Set<AnyHashable> = [1, 2, "3"]
s as! Set<Int>

StaticString.swift

1
2
3
4
5
6
7
8
9
10
11
// * a pointer to a null-terminated sequence of UTF-8 code units:
let emoji: StaticString = "\u{1F600}"
emoji.hasPointerRepresentation // -> true
emoji.isASCII // -> false
// emoji.unicodeScalar // -> Fatal error!
emoji.utf8CodeUnitCount // -> 4
emoji.utf8Start[0] // -> 0xF0
emoji.utf8Start[1] // -> 0x9F
emoji.utf8Start[2] // -> 0x98
emoji.utf8Start[3] // -> 0x80
emoji.utf8Start[4] // -> 0x00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// * a single Unicode scalar value, under very limited circumstances:
struct MyStaticScalar: ExpressibleByUnicodeScalarLiteral {
typealias UnicodeScalarLiteralType = StaticString
let value: StaticString
init(unicodeScalarLiteral value: StaticString) {
self.value = value
}
}

let emoji: StaticString = MyStaticScalar("\u{1F600}").value
emoji.hasPointerRepresentation // -> false
emoji.isASCII // -> false
emoji.unicodeScalar.value // -> 0x1F600
emoji.utf8CodeUnitCount // -> Fatal error!
emoji.utf8Start // -> Fatal error!

hasPointerRepresentation 一个布尔值,表示静态字符串是否存储指向以 null 结尾的 UTF-8代码单元序列的指针。

  • true:这时访问unicodeScalar会触发运行时错误;
  • false:静态字符串存储单个 Unicode 标量值。这时访问utf8开头的属性会触发运行时错误;

第三部分:树

第十章:树

树在数据结构中相当重要。它用于解决软件开发中的许多反复出现的挑战:

  • 表示层次关系。

  • 管理排序的数据。

  • 促进快速查找操作。

相关术语

  • 节点(Node)
  • 父子(Parent and child)
  • 根(Root)
  • 叶子(Leaf)

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TreeNode<T> {

public var value: T
public var children: [TreeNode] = []

public init(_ value: T) {
self.value = value
}

public func add(_ child: TreeNode) {
children.append(child)
}
}

遍历算法

遍历线性的集合(如:数组和链表)是非常直接的。因为有直接的首尾。

但是遍历树就复杂了。是左边优先,还是深度优先,要看具体解决的问题。

深度优先遍历(Depth-first traversal)
1
2
3
4
5
6
7
8
extension TreeNode {
public func forEachDepthFirst(visit: (TreeNode) -> Void) {
visit(self)
children.forEach {
$0.forEachDepthFirst(visit: visit)
}
}
}
层级优先遍历(Level-order traversal)
1
2
3
4
5
6
7
8
9
10
11
extension TreeNode {
public func forEachLevelOrder(visit: (TreeNode) -> Void) {
visit(self)
var queue = Queue<TreeNode>()
children.forEach { queue.enqueue($0) }
while let node = queue.dequeue() {
visit(node)
node.children.forEach { queue.enqueue($0) }
}
}
}
搜索
1
2
3
4
5
6
7
8
9
10
11
extension TreeNode where T: Equatable {
public func search(_ value: T) -> TreeNode? {
var result: TreeNode?
forEachDepthFirst { node in
if node.value == value {
result = node
}
}
return result
}
}

总结

  1. 树与链接列表具有一些相似之处,但是,链接列表节点只能链接到另一个节点,而树节点可以链接到无限多个节点。
  2. 熟悉树的术语,例如父,子,叶子和根。 这些术语中有许多是其他程序员常用的语言,将用于帮助解释其他树形结构。
  3. 遍历(例如深度优先遍历和层级遍历)不是特定于常规树的。 它们也可以在其他树上工作,尽管根据树的结构,它们的实现会略有不同。

第十一章:树挑战

打印所有的值,按照他们层级的顺序。同意层级应该打印在一行

例如:

1
2
3
15 
1 17 20
1 5 0 2 5 7

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func printEachLevel<T>(for tree: TreeNode<T>) {
var queue = Queue<TreeNode<T>>()

/// 加入每层的节点计数,当打印完时,换行
var nodesLeftInCurrentLevel = 0

queue.enqueue(tree)
while !queue.isEmpty {
nodesLeftInCurrentLevel = queue.count
while nodesLeftInCurrentLevel > 0 {
guard let node = queue.dequeue() else { break }
print("\(node.value) ", terminator: "")
node.children.forEach { queue.enqueue($0) }
nodesLeftInCurrentLevel -= 1
}
print()
}
}

第一章:引言

Kitura是什么?

在词源学上,Kitura这个词松散地源于希伯来语Keturah,字面意思是“香”。

为什么选择Swift写后端?

支持为服务器编写Swift的核心论点是在应用程序的前端和后端之间共享代码和知识。如果你是一个团队,这种精简可以说是更加关键和赋权。这包括但不限于模型对象代码。

BFF(Backonnd for Frontend)是一种设计模式,很快就会成为你最好的朋友(永远?)。

这个模式在这个特定用例中起作用的几个原因:

  1. 你可以确定从此API发出请求的唯一设备将是iOS设备。这意味着你可以更少担心平衡请求负载。

  2. 你可以更好地控制对iOS设备的响应中的数据。在此之前,你可能已经等待后端开发人员最终帮助你,并根据User-Agent首部有条件地修剪响应数据。现在,你可以让后端开发人员不间断地工作,并通过将响应数据解析为你的BFF设备所需的内容来解决你的问题。

  3. 在我们的特定用例中,你可以在发送响应之前保存一个用户可能对数据库进行的查询的响应。这意味着,如果其他人在相同的位置请求餐馆信息(例子是请求包含地理位置的餐馆信息),并且存储的数据足够新以满足你的刷新策略,你甚至不必向主服务器发送查询!你只需发送缓存的数据,并相信它对你的用户来说足够新。

  4. 你可以使用你已经知道如何在Swift中编写代码的语言完成1-3!

第二章:Hello, World!

如何Kitura项目

先安装Kitura macOS App和Kitura CLI

1
2
brew tap ibm-swift/kitura
brew install kitura
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kitura
Usage: kitura [options] [command]

Kitura command-line interface

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
build build the project in a local container
create interactively create a Kitura project
idt install IBM Cloud Developer Tools
init scaffold a bare-bones Kitura project
kit print Cocoapods boilerplate for KituraKit
run run the project in a local container
sdk generate a client SDK from an OpenAPI/Swagger spec
help [cmd] display help for [cmd]

创建的Kitura项目提供了一个开箱即用的全功能Kitura应用程序。 它提供了在生产环境中运行的任何应用程序可能需要的一些功能,包括:

  • 使用HeliumLogger记录信息,警告和错误消息。

  • 使用CloudEnvironment进行动态配置查找和设置。

  • 使用Health查看和报告应用程序健康状况。

  • 使用SwiftMetrics进行App和Kitura框架指标和监控。

项目使用SPM管理依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// swift-tools-version:4.0
import PackageDescription

let package = Package(
name: "EmojiJournalServer",
dependencies: [
.package(url: "https://github.com/IBM-Swift/Kitura.git", .upToNextMinor(from: "2.5.0")),
.package(url: "https://github.com/IBM-Swift/HeliumLogger.git", from: "1.7.1"),
.package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "9.0.0"),
.package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.0.0"),
.package(url: "https://github.com/IBM-Swift/Health.git", from: "1.0.0"),
],
targets: [
.target(name: "EmojiJournalServer", dependencies: [ .target(name: "Application"), "Kitura" , "HeliumLogger"]),
.target(name: "Application", dependencies: [ "Kitura", "CloudEnvironment","SwiftMetrics","Health", ]),

.testTarget(name: "ApplicationTests" , dependencies: [.target(name: "Application"), "Kitura","HeliumLogger" ])
]
)

Application.swift包含应用程序的生命周期处理,提供emojiJournalServer目标中main.swift使用的核心init()run()方法来启动应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Foundation
import Kitura
import LoggerAPI
import Configuration
import CloudEnvironment
import KituraContracts
import Health

public let projectPath = ConfigurationManager.BasePath.project.path
public let health = Health()

public class App {
let router = Router()
let cloudEnv = CloudEnv()

public init() throws {
// Run the metrics initializer
initializeMetrics(router: router)
}

func postInit() throws {
// Endpoints
initializeHealthRoutes(app: self)
router.get("/", handler: helloWorldHandler)
}

func helloWorldHandler(request: RouterRequest, response: RouterResponse, next: ()->()) {
response.send("Hello, World!")
next()
}

public func run() throws {
try postInit()
Kitura.addHTTPServer(onPort: cloudEnv.port, with: router)
Kitura.run()
}
}

运行后

欢迎页:http://localhost:8090

健康:http://localhost:8090/health

监控仪表板:http://localhost:8090/swiftmetrics-dash

HTTP吞吐量视图使用每秒请求(RPS)度量标准显示在任何时间点发生的请求量。

http://localhost:8090/metrics

使用Docker在Linux上运行应用程序

Docker和Kubernetes是Kitura部署的核心,它们提供了重要的性能和扩展优势。

1
2
kitura build
kitura run

第三章:RESTful APIs

在开始构建这些RESTful API之前,将了解RESTful API本身,包括它们的架构方法和设计原则。

REpresentational State Transfer (REST)

REpresentational State Transfer(REST)是一种架构风格或设计模式的API。 如果一个API符合Roy Fielding博士在他的2000年论文“架构风格和基于网络的软件架构设计”中提出的一系列约束,那么它就是RESTful。

Fielding提出了以下六个限制:

  1. 客户端 - 服务器

RESTful API在客户端和服务器之间提供明确的分离,允许客户端和服务器彼此独立地开发。这种分离使得为同一服务器实现多个客户端成为可能,就像你为EmojiJournal创建iOS和Web应用程序客户端时所做的那样。

  1. 无状态

RESTful API应该是无状态的。每个请求都应该是自包含的,并且不应该依赖于存储在服务器上的先前请求的任何会话上下文。

  1. 可缓存

由于RESTful API是无状态和自包含的,因此可以将请求的结果缓存到RESTful API。这可以由客户端或诸如web代理之类的中介来完成。

设计良好的RESTful API应该鼓励尽可能存储可缓存的数据,对请求的响应被隐式或显式标记为可缓存,可缓存到特定时间(expires-at)或不可缓存。

  1. 分层系统

RESTful API可以是分层的,因此客户端不知道它是直接连接到API本身还是通过代理或负载均衡器间接连接。另外,API本身可以由服务器系统本身内的若干层或源组成和构建。

  1. 按需代码(可选)

RESTful API可以提供可以直接在客户端上运行的功能。实际上,这通常仅限于在浏览器中运行的JavaScript或Java Applet。

  1. 统一界面

RESTful API最重要的概念之一是资源的使用。资源可以是API可以提供有关信息的任何对象,例如你将使用的JournalEntry资源。

提供统一接口的RESTful API必须满足以下四个约束:

  • 请求必须确定他们采取行动的资源。
  • 请求和响应必须使用资源的表示。
  • 消息必须是自描述的,自包含的,并且只能使用标准操作。
  • 资源必须通过链接连接到其他资源。

另请注意,没有提及HTTP和Web请求。虽然RESTful API最常使用HTTP实现,但这不是必需的。也就是说,我们将仅介绍如何使用标准HTTP操作构建RESTful API,我们接下来将简要介绍HTTP本身。

HyperText Transfer Protocol (HTTP)

超文本传输协议(HTTP)是一种客户端 - 服务器,基于请求 - 响应的协议,是网站和基于Web的流量的基础。 因此,它受到客户端和服务器的普遍支持。

HTTP-based RESTful APIs

URL作为资源

统一接口的第一个要求是请求标识它们所依赖的资源。在HTTP中,这是通过RESTful API使用请求的URL实现的。

例如:

1
/entries

特定资源的URL编码标识符

将提供这些URL以使客户端能够与所有日记帐分录和特定日记帐分录进行交互:

1
2
/entries 
/entries/<identifier>

用于资源组的URL编码查询参数

你可能还希望提供与资源子集的API。 这再次通过URL完成,这次使用URL查询参数。

URL查询参数以?开头。 他们可以使用<key> = <value>格式的键值对传递有关资源的任何其他信息。 可以使用键值对之间的分隔符聚合和传递多个键值对。

1
2
3
/entries 
/entries?emoji=<emoji>&date=<date>
/entries/<idenfifier>

分层API的分层URL

RESTful URL也可以是分层的。 在你的EmojiJournal应用程序中,你只会支持拥有单个EmojiJournal的用户。 但是,假设你希望支持拥有多个期刊的用户,每个期刊都有自己的日记帐分录。

1
2
3
4
/journals 
/journals/<identifier>
/journals/<identifier>/entries
/journals/<identifier>/entries?emoji=<emoji>&date=<date> /journals/<identifier>/entries/<identifier>

例如,对以下URL的客户端请求将与日记2中的条目20进行交互:

1
/journals/2/entries/20

资源表示

回想一下,统一接口的第二个概念是请求和响应应该使用资源的表示。 这是通过在HTTP正文数据中编码资源的表示,在基于HTTP的RESTful API中实现的。

HTTP消息,无论是请求还是响应,基本上由两部分组成:一组标题和正文数据。 为了完整性,响应还包含一个状态代码,用于报告请求的状态。

HTTP首部和正文数据

HTTP首部为请求和响应提供了额外的元数据。 首部在请求或响应开始时传输,并存储为键值对,一些首部允许多个值。

你可以使用HTTP首部存储你希望的任何其他信息。 你应该了解两个正式定义的标准标题,请参阅https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html(由正式的核心RFC规范定义),以及不太正式但广泛使用的非标准的首部,请参阅https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Common_non-standard_request_fields

标准头用于传递诸如Cookie,授权,ContentType(描述正文数据的媒体类型)和Content-Length(以字节为单位描述正文数据长度)等信息。

HTTP消息正文包含与请求或响应关联的数据。 在响应对网页的请求的情况下,正文数据包含网页本身的来源。 看一下对Hello,World的请求生成的HTTP响应! 你在上一章中创建的应用程序。

正文数据中的资源表示

如前所述,RESTful API对返回的HTTP正文数据中的资源表示进行编码。 身体通常由三部分组成:

  1. Content-Type首部

Content-Type首部表示用于表示正文数据中的资源的编码。最常用的方法是使用JavaScript Object Notation(JSON)对表示进行编码,并将Content-Type首部设置为application/json。有时与RESTful API一起使用的身体数据编码的其他示例包括Protobuf,Thrift和Avro。

  1. 接受首部(仅限请求)

接受首部指示客户端将接受响应的编码。这通常是application/json,但它也可以包含编码列表。例如,如果客户端在JSON和Protobuf中都接受了响应,则此值可能设置为:application/json;application/protobuf

  1. 编码的正文数据

最后,使用Content-Type首部中设置的编码将资源本身编码为正文数据。在最常见的application/json场景中,这是资源的JSON表示。 JSON的细节取决于特定资源的模型。例如,在本书后面稍后,JournalEntry的资源将包含id,emoji和date,类似于:

1
2
3
4
5
{
"id":"1",
"emoji":"😊 ",
"date":"2018-09-18T00:07:01Z"
}

自我描述性的信息

你已经了解了资源的识别和表示,但你如何对这些资源进行操作?

统一接口的第三个原则解决了这个问题。 它规定请求和响应都应该是自描述的,并且它们仅使用客户端和服务器都理解的标准操作。 在基于HTML的RESTful API中,这是通过使用特定的HTTP方法实现的。

RFC 2616概述了八种HTTP方法:OPTIONS,GET,HEAD,POST,PUT,DELETE,TRACE和CONNECT。

通过链接连接资源

统一接口的第四个也是最后一个概念是资源应该通过链接连接到其他资源。

如你所见,基于HTTP的RESTful API使用URL和基于URL的标识符来提供对资源的访问并指定单个资源。 因此,资源之间的链接只是进一步的RESTful URL和链接到连接资源的基于URL的标识符。

有两种常见的场景需要URL或标识资源:

  • 请求的资源引用另一个(子或相关)资源。
  • 你需要知道资源的标识符。

构建RESTful API

image-20190806103823282

到目前为止,你已经学会了所有基础知识。 当你了解有关设计和实现RESTful API的更多信息时,你会发现除了我们在此处介绍的范围之外还有其他最佳实践。 这包括设置正确的Content-Type和Accepts标头,为成功创建设置Location标头,并为请求中的任何故障设置正确的HTTP状态代码。

此外,需要对请求和响应主体进行编码和解码,最常见的是来自JSON。 这曾经是一项艰巨的任务。 由于强大的Codable协议,Swift使JSON优雅且无痛。

第四章:Codable简介

Codable协议是Swift编程语言最强大的功能之一。 在一个句子中,当你扩展对象以符合Codable时,你可以使该对象自动序列化为任何外部可读格式,例如JSON,XML或协议缓冲区。

The bare necessities

直接看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import Foundation

// 1 Cadabe 是一个组合的协议:Decodable & Encodable

public struct Song: Codable {
var title: String
var length: Int

enum CodingKeys: String, CodingKey {
case title = "songTitle"
case length = "songLength"
}
}

public struct Animal: Codable {
var name: String
var age: Int?
var isFriendly: Bool
var birthday: Date
var songs: [Song]
}

// 2 嵌套
let baloo = Animal(name: "Baloo",
age: 5,
isFriendly: true,
birthday: Date(),
songs: [Song(title: "The Bare Necessities", length: 180)])
let bagheera = Animal(name: "Bagheera",
age: nil,
isFriendly: true,
birthday: Date(),
songs: [Song(title: "Jungle's No Place For A Boy", length: 95)])

// 3 处理日期格式,驼峰式与蛇式转换
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

do {
// 4
let encodedBaloo = try encoder.encode(baloo)
if let balooString = String(data: encodedBaloo, encoding: .utf8) {
print(balooString)
}
let encodedBagheera = try encoder.encode(bagheera)
if let bagheeraString = String(data: encodedBagheera, encoding: .utf8) {
print(bagheeraString)
}
// 5
let decodedBaloo = try decoder.decode(Animal.self, from: encodedBaloo)
print(decodedBaloo)
let decodedBagheera = try decoder.decode(Animal.self, from: encodedBagheera)
print(decodedBagheera)
} catch let error {
print("Error occurred: \(error.localizedDescription)")
}

第五章:Codable Routing

Codable Routing支持以完全类型安全的方式在你的应用程序的结构和类以及HTTP请求和响应中使用的正文数据之间自动转换。 它大大减少了构建路由处理程序时需要编写的代码,并使Kitura能够代表你执行数据验证和错误处理。

看Model和Route的实现:

1
2
3
4
5
6
7
import Foundation

struct JournalEntry: Codable {
var id: String?
var emoji: String
var date: Date
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation
import LoggerAPI
import Kitura

var entries: [JournalEntry] = []

func initializeEntryRoutes(app: App) {
app.router.get("/entries", handler: getAllEntries)
app.router.post("/entries", handler: addEntry)
app.router.delete("/entries", handler: deleteEntry)
Log.info("Journal entry routes created")
}

func addEntry(entry: JournalEntry, completion: @escaping (JournalEntry?, RequestError?) -> Void) {
var storedEntry = entry
storedEntry.id = entries.count.value
entries.append(storedEntry)
completion(storedEntry, nil)
}

func getAllEntries(completion: @escaping ([JournalEntry]?, RequestError?) -> Void) -> Void {
completion(entries, nil)
}

func deleteEntry(id: String, completion: @escaping (RequestError?) -> Void) {
guard let index = entries.index(where: { $0.id == id }) else {
return completion(.notFound)
}
entries.remove(at: index)
completion(nil)
}

Codable Routing的一个独特功能是它只需要你在处理函数中定义你希望从请求中接收的值,以及你将通过完成处理程序添加到响应中的值。

目前,Kitura仅支持从application/json解码。 这意味着,实际上,Content-Type当前必须始终设置为application/json。 但是,Kitura团队计划在即将发布的版本中支持其他编码类型。

第六章:OpenAPI规范

Swagger的目标

在Tony Tam开创性的开源项目的基础上,其他公司开始做出贡献。 最终,Linux基金会承担了该项目的赞助,并将其名称更改为OpenAPI Initiative。 这种广泛的支持使OpenAPI规范在开源软件开发中占据了非常突出的位置。

当Tony开始研究Swagger API项目时,他确定了三个关键目标:

  • API开发
  • API文档
  • API交互

生成你的规格

通过添加Kitura-OpenAPI依赖

1
.package(url: "https://github.com/IBM-Swift/Kitura-OpenAPI.git", from: "1.1.1"),

运行后

http://localhost:8080/openapi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
{
"schemes" : [
"http"
],
"swagger" : "2.0",
"info" : {
"version" : "1.0",
"title" : "Kitura Project",
"description" : "Generated by Kitura"
},
"paths" : {
"\/entries" : {
"get" : {
"responses" : {
"200" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#\/definitions\/JournalEntry"
}
},
"description" : "successful response"
}
},
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
]
},
"post" : {
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
],
"responses" : {
"200" : {
"schema" : {
"$ref" : "#\/definitions\/JournalEntry"
},
"description" : "successful response"
}
},
"parameters" : [
{
"in" : "body",
"name" : "input",
"required" : true,
"schema" : {
"$ref" : "#\/definitions\/JournalEntry"
}
}
]
}
},
"\/health" : {
"get" : {
"responses" : {
"200" : {
"schema" : {
"$ref" : "#\/definitions\/Status"
},
"description" : "successful response"
}
},
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
]
}
},
"\/entries\/{id}" : {
"delete" : {
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
],
"responses" : {
"200" : {
"description" : "successful response"
}
},
"parameters" : [
{
"in" : "path",
"name" : "id",
"required" : true,
"type" : "string"
}
]
}
}
},
"basePath" : "\/",
"definitions": {
"JournalEntry": {
"type": "object",
"required": ["date","emoji"],
"properties": {
"id": {"type":"string"},
"emoji": {"type":"string"},
"date": {"type":"number"}
}
},
"Status": {
"type": "object",
"required": ["status","details","timestamp"],
"properties": {
"status": {"type":"string"},
"details": {"items":{"type":"string"},"type":"array"},
"timestamp": {"type":"string"}
}
}
}
}

此代码段的顶部表示你正在查看路径/entries的GET路由。 三个子节点描述了这条路线:

  • “responses”提供用户可以接收的一系列响应。

  • “consumes”指定必须给出此方法的数据类型。

  • “produces”描述响应将包含的数据类型。

使用Kitura OpenAPI UI

同步生成的还有UI调试界面

http://localhost:8080/openapi/ui

image-20190807140446505

这对于你需要进行快速而肮脏的测试的情况非常有用 - 或者如果你无法让顽固的队友相信这个模块真的有用!

为你的iOS应用生成SDK

终端运行,即可生成SDK,一个功能齐全的Swift SDK,可以处理与此特定服务器的所有网络通信。

1
2
3
4
5
6
docker pull swaggerapi/swagger-codegen-cli
docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli langs
mkdir GeneratedSDK
touch specification.json
// 复制http://localhost:8080/openapi的内容到specification.json
docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i /local/specification.json -l swift -o /local/GeneratedSDK

在实践中,这不仅可以节省时间并让你快速启动和运行,还可以节省错误,并且对于那些没有(喘气)在Swift工作的人来说尤其有用!

0%