Coding01

Coding 点滴

0%

学习 iOS Widgets 开发之股票小组件(二)

继续上次学习 iOS Widgets 开发之股票小组件(一),今天主要是完成如下图所示的显示效果。

基本在 View 视图上分成三个部分:

  1. 左边股票 name 和股票编号 symbol;
  2. 中间部分是股票走势图;
  3. 右边是实时价格 price 和涨跌幅。

股票列表

主界面直接使用 VStack 布局,内置 GeometryReader 组件,用于将 frame 传给 LineView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VStack(alignment: HorizontalAlignment.leading) {
ForEach(kLines, id: \.self) { kLine in
GeometryReader { reader in
LineView(
data: kLine.prices(),
symbol: kLine.symbol,
name: kLine.name,
price: kLine.lastPrice(),
yestclose: kLine.yestclose,
compute: kLine.compute(),
height: self.height / CGFloat(kLines.count)
).frame(width: reader.frame(in: .local).width, height: self.height/CGFloat(kLines.count), alignment: Alignment.center)
}
}
}

因为 Widget 宽高都是固定的,所以我们根据关注的股票数量,均分我们每支股票的高度 height,这里就借助 GeometryReader 的力量。

GeometryReader

A container view that defines its content as a function of its own size and coordinate space.

— 一个容器View,根据其自身大小和坐标定义其内容。

1
@frozen struct GeometryReader<Content> where Content : View

设置 frame 具体代码:

1
frame(width: reader.frame(in: .local).width, height: self.height/CGFloat(kLines.count), alignment: Alignment.center)

LineView

如开始说的,每支股票的结构分成三部分,代码如下:

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
HStack(alignment: .center) {
VStack(alignment: .leading) {
if (self.name != nil) {
Text(self.name!)
.font(.system(size: 14, weight: Font.Weight.bold, design: Font.Design.default))
.foregroundColor(.font_foreground)

Text(self.symbol!)
.font(.system(size: 11, weight: Font.Weight.medium, design: Font.Design.default))
.foregroundColor(.main_color_1)
} else {
Text(self.symbol!)
.font(.system(size: 14, weight: Font.Weight.medium, design: Font.Design.default))
.foregroundColor(.font_foreground)
}
}

Spacer().frame(width: 75)

GeometryReader { reader in
Line(data: self.data,
yestclose: self.yestclose,
frame: .constant(reader.frame(in: .local))
).offset(x: 0, y: height/2)
}


Spacer().frame(width: 30)

VStack(alignment: .leading) {
Text(String(format: "%.3f", self.price))
.font(.system(size: 14, weight: Font.Weight.bold, design: Font.Design.default))
.foregroundColor(.font_foreground)
Text(String(format: "%.2f", self.compute) + "%")
.font(.system(size: 11, weight: Font.Weight.bold, design: Font.Design.default))
.foregroundColor(self.compute > 0 ? .red : .green)
}
}.lineLimit(1)
.padding(EdgeInsets.init(top: 15, leading: 15, bottom: 15, trailing: 15))
}

这里借助 Spacer() 将三个结构分开。

A flexible space that expands along the major axis of its containing stack layout, or on both axes if not contained in a stack.

通过填充间隔空间,并指定宽度,这样能保证整个结构稳定,不会随着走势图的数据多或者少影响到整个空间格局。

至于左右两边的股票信息布局,看代码就好。

股票走势图 Line

股票走势图的代码我主要参考Create a Line Chart in SwiftUI Using Paths

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
import SwiftUI

struct LineView: View {
var data: [(Double)]
var symbol: String?
var name: String?
var price: Double
var yestclose: Double
var compute: Double
var height: CGFloat

public var body: some View {
HStack(alignment: .center) {
VStack(alignment: .leading) {
if (self.name != nil) {
Text(self.name!)
.font(.system(size: 14, weight: Font.Weight.bold, design: Font.Design.default))
.foregroundColor(.font_foreground)

Text(self.symbol!)
.font(.system(size: 11, weight: Font.Weight.medium, design: Font.Design.default))
.foregroundColor(.main_color_1)
} else {
Text(self.symbol!)
.font(.system(size: 14, weight: Font.Weight.medium, design: Font.Design.default))
.foregroundColor(.font_foreground)
}
}

Spacer().frame(width: 75)

GeometryReader { reader in
Line(data: self.data,
yestclose: self.yestclose,
frame: .constant(reader.frame(in: .local))
).offset(x: 0, y: height/2)
}


Spacer().frame(width: 30)

VStack(alignment: .leading) {
Text(String(format: "%.3f", self.price))
.font(.system(size: 14, weight: Font.Weight.bold, design: Font.Design.default))
.foregroundColor(.font_foreground)
Text(String(format: "%.2f", self.compute) + "%")
.font(.system(size: 11, weight: Font.Weight.bold, design: Font.Design.default))
.foregroundColor(self.compute > 0 ? .red : .green)
}
}.lineLimit(1)
.padding(EdgeInsets.init(top: 15, leading: 15, bottom: 15, trailing: 15))
}
}

//struct LineView_Previews: PreviewProvider {
// static var previews: some View {
// LineView(data: [8,23,54,32,12,37,7,23,43], symbol: 200.0, name: "Full chart", price: 23)
// }
//}

struct Line: View {
var data: [(Double)]
var yestclose: Double

@Binding var frame: CGRect

let padding:CGFloat = 10

var stepWidth: CGFloat {
if data.count < 2 {
return 0
}
return frame.size.width / CGFloat(data.count - 1)
}

var stepHeight: CGFloat {
var min: Double?
var max: Double?
let points = self.data
if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint {
min = minPoint
max = maxPoint
} else {
return 0
}
if let min = min, let max = max, min != max {
return (frame.size.height - padding) / CGFloat(max - min)
}

return 0
}
var path: Path {
let points = self.data
return Path.lineChart(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
}

var yesterDayPath: Path {
let points = [Double](repeating: self.yestclose, count: self.data.count)
return Path.lineDotted(points: points, step: CGPoint(x: stepWidth, y: stepHeight), min: self.data.min() ?? 0)
}

public var body: some View {
ZStack {
self.path
.stroke(Color.font_foreground ,style: StrokeStyle(lineWidth: 1, lineJoin: .round))
.drawingGroup()

self.yesterDayPath
.stroke((self.data.last ?? 0) > self.yestclose ? Color.red : Color.green ,style: StrokeStyle(lineWidth: stepWidth, dash: [stepWidth/2]))
.drawingGroup(opaque: false, colorMode: ColorRenderingMode.extendedLinear)
}
}
}

主要有两条 path:

1
2
3
4
5
6
7
8
9
ZStack {
self.path
.stroke(Color.font_foreground ,style: StrokeStyle(lineWidth: 1, lineJoin: .round))
.drawingGroup()

self.yesterDayPath
.stroke((self.data.last ?? 0) > self.yestclose ? Color.red : Color.green ,style: StrokeStyle(lineWidth: stepWidth, dash: [stepWidth/2]))
.drawingGroup(opaque: false, colorMode: ColorRenderingMode.extendedLinear)
}

一条走势图,一条借助前一天的收盘价作标志线,可以看到我们关心的股票是涨还是跌了。

走势图做法也比较简单,把每个点串联起来,把所有点的股价 price 和我们的高度 height 绑定在一起,计算出每一步的 step(width, height)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var stepWidth: CGFloat {
if data.count < 2 {
return 0
}
return frame.size.width / CGFloat(data.count - 1)
}

var stepHeight: CGFloat {
var min: Double?
var max: Double?
let points = self.data
if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint {
min = minPoint
max = maxPoint
} else {
return 0
}
if let min = min, let max = max, min != max {
return (frame.size.height - padding) / CGFloat(max - min)
}

return 0
}

扩展 Path:

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
extension Path {

static func lineChart(points:[Double], step:CGPoint) -> Path {
var path = Path()
if (points.count < 2) {
return path
}
guard let offset = points.min() else { return path }
let p1 = CGPoint(x: 0, y: -CGFloat(points[0] - offset) * step.y)
path.move(to: p1)
for pointIndex in 1..<points.count {
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: -step.y * CGFloat(points[pointIndex] - offset))
path.addLine(to: p2)
}
return path
}

static func lineDotted(points:[Double], step:CGPoint, min: Double) -> Path {
var path = Path()

let p1 = CGPoint(x: 0, y: -CGFloat(points[0] - min) * step.y)
path.move(to: p1)

let p2 = CGPoint(x: step.x * CGFloat(points.count), y: -step.y * CGFloat(points[0] - min))
path.addLine(to: p2)

return path
}

}

总结

有了 View 效果,基本完成了股票小组件的开发,接下来就是增加自定义数据的环节了。

Welcome to my other publishing channels