├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ └── aheze.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ ├── 100.png
│ ├── 1024.png
│ ├── 114.png
│ ├── 120.png
│ ├── 144.png
│ ├── 152.png
│ ├── 167.png
│ ├── 180.png
│ ├── 20.png
│ ├── 29.png
│ ├── 40.png
│ ├── 50.png
│ ├── 57.png
│ ├── 58.png
│ ├── 60.png
│ ├── 72.png
│ ├── 76.png
│ ├── 80.png
│ ├── 87.png
│ └── Contents.json
├── Contents.json
├── ImageCode.imageset
│ ├── Contents.json
│ └── Screen Shot 2022-01-12 at 12.52.31 PM.png
├── Logo.imageset
│ ├── 1024.png
│ └── Contents.json
├── SwiftUI.imageset
│ ├── Contents.json
│ └── swiftui-96x96_2x.png
├── TextCode.imageset
│ ├── Contents.json
│ └── Screen Shot 2022-01-12 at 12.52.18 PM.png
└── VStackCode.imageset
│ ├── Contents.json
│ └── Screen Shot 2022-01-12 at 12.52.41 PM.png
├── AugmentedRealityView.swift
├── Container.swift
├── ContentView.swift
├── Demo.mp4
├── GameView.swift
├── LICENSE
├── MyApp.swift
├── Package.swift
├── README.md
├── TodoListView.swift
└── TutorialView.swift
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/aheze.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | ClubDay2022.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | AppModule
16 |
17 | primary
18 |
19 |
20 | ClubDay2022
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "29.png",
17 | "idiom" : "iphone",
18 | "scale" : "1x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "58.png",
23 | "idiom" : "iphone",
24 | "scale" : "2x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "87.png",
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "80.png",
35 | "idiom" : "iphone",
36 | "scale" : "2x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "3x",
43 | "size" : "40x40"
44 | },
45 | {
46 | "filename" : "57.png",
47 | "idiom" : "iphone",
48 | "scale" : "1x",
49 | "size" : "57x57"
50 | },
51 | {
52 | "filename" : "114.png",
53 | "idiom" : "iphone",
54 | "scale" : "2x",
55 | "size" : "57x57"
56 | },
57 | {
58 | "filename" : "120.png",
59 | "idiom" : "iphone",
60 | "scale" : "2x",
61 | "size" : "60x60"
62 | },
63 | {
64 | "filename" : "180.png",
65 | "idiom" : "iphone",
66 | "scale" : "3x",
67 | "size" : "60x60"
68 | },
69 | {
70 | "filename" : "20.png",
71 | "idiom" : "ipad",
72 | "scale" : "1x",
73 | "size" : "20x20"
74 | },
75 | {
76 | "filename" : "40.png",
77 | "idiom" : "ipad",
78 | "scale" : "2x",
79 | "size" : "20x20"
80 | },
81 | {
82 | "filename" : "29.png",
83 | "idiom" : "ipad",
84 | "scale" : "1x",
85 | "size" : "29x29"
86 | },
87 | {
88 | "filename" : "58.png",
89 | "idiom" : "ipad",
90 | "scale" : "2x",
91 | "size" : "29x29"
92 | },
93 | {
94 | "filename" : "40.png",
95 | "idiom" : "ipad",
96 | "scale" : "1x",
97 | "size" : "40x40"
98 | },
99 | {
100 | "filename" : "80.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "40x40"
104 | },
105 | {
106 | "filename" : "50.png",
107 | "idiom" : "ipad",
108 | "scale" : "1x",
109 | "size" : "50x50"
110 | },
111 | {
112 | "filename" : "100.png",
113 | "idiom" : "ipad",
114 | "scale" : "2x",
115 | "size" : "50x50"
116 | },
117 | {
118 | "filename" : "72.png",
119 | "idiom" : "ipad",
120 | "scale" : "1x",
121 | "size" : "72x72"
122 | },
123 | {
124 | "filename" : "144.png",
125 | "idiom" : "ipad",
126 | "scale" : "2x",
127 | "size" : "72x72"
128 | },
129 | {
130 | "filename" : "76.png",
131 | "idiom" : "ipad",
132 | "scale" : "1x",
133 | "size" : "76x76"
134 | },
135 | {
136 | "filename" : "152.png",
137 | "idiom" : "ipad",
138 | "scale" : "2x",
139 | "size" : "76x76"
140 | },
141 | {
142 | "filename" : "167.png",
143 | "idiom" : "ipad",
144 | "scale" : "2x",
145 | "size" : "83.5x83.5"
146 | },
147 | {
148 | "filename" : "1024.png",
149 | "idiom" : "ios-marketing",
150 | "scale" : "1x",
151 | "size" : "1024x1024"
152 | }
153 | ],
154 | "info" : {
155 | "author" : "xcode",
156 | "version" : 1
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Assets.xcassets/ImageCode.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Screen Shot 2022-01-12 at 12.52.31 PM.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Assets.xcassets/ImageCode.imageset/Screen Shot 2022-01-12 at 12.52.31 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/ImageCode.imageset/Screen Shot 2022-01-12 at 12.52.31 PM.png
--------------------------------------------------------------------------------
/Assets.xcassets/Logo.imageset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/Logo.imageset/1024.png
--------------------------------------------------------------------------------
/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "1024.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Assets.xcassets/SwiftUI.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "swiftui-96x96_2x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Assets.xcassets/SwiftUI.imageset/swiftui-96x96_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/SwiftUI.imageset/swiftui-96x96_2x.png
--------------------------------------------------------------------------------
/Assets.xcassets/TextCode.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Screen Shot 2022-01-12 at 12.52.18 PM.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Assets.xcassets/TextCode.imageset/Screen Shot 2022-01-12 at 12.52.18 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/TextCode.imageset/Screen Shot 2022-01-12 at 12.52.18 PM.png
--------------------------------------------------------------------------------
/Assets.xcassets/VStackCode.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Screen Shot 2022-01-12 at 12.52.41 PM.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Assets.xcassets/VStackCode.imageset/Screen Shot 2022-01-12 at 12.52.41 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Assets.xcassets/VStackCode.imageset/Screen Shot 2022-01-12 at 12.52.41 PM.png
--------------------------------------------------------------------------------
/AugmentedRealityView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AugmentedRealityView.swift
3 | // SwiftPlaygrounds
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/10/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | import ARKit
10 | import SwiftUI
11 |
12 | class ARViewModel: ObservableObject {
13 | var addText: (() -> Void)?
14 | var addSphere: (() -> Void)?
15 | var addSprinkler: (() -> Void)?
16 | var clear: (() -> Void)?
17 | var resume: ((Bool) -> Void)?
18 | }
19 |
20 | struct AugmentedRealityButton: View {
21 | var image: String
22 | var text: String
23 | let action: () -> Void
24 |
25 | var body: some View {
26 | Button(action: action) {
27 | VStack(spacing: 10) {
28 | Image(systemName: image)
29 | .foregroundColor(.purple)
30 | .font(.title2)
31 |
32 | Text(text)
33 | .foregroundColor(Color(uiColor: .label))
34 | }
35 | .font(.title3)
36 | .frame(maxWidth: .infinity, maxHeight: .infinity)
37 | .background(Color(uiColor: .systemBackground))
38 | .cornerRadius(16)
39 | }
40 | }
41 | }
42 |
43 | struct AugmentedRealityView: View {
44 | let focused: Bool
45 | @StateObject var model = ARViewModel()
46 |
47 | var body: some View {
48 | VStack(spacing: 0) {
49 | ARViewControllerRepresentable(model: model)
50 | .ignoresSafeArea()
51 | .overlay(alignment: .topTrailing) {
52 | Button {
53 | model.clear?()
54 | } label: {
55 | Image(systemName: "xmark")
56 | .foregroundColor(.white)
57 | .font(.largeTitle)
58 | .frame(width: 64, height: 64)
59 | .background(.white.opacity(0.3))
60 | .cornerRadius(32)
61 | .padding()
62 | }
63 | }
64 |
65 | HStack {
66 | AugmentedRealityButton(image: "textformat", text: "Text") {
67 | model.addText?()
68 | }
69 | AugmentedRealityButton(image: "circle.fill", text: "Sphere") {
70 | model.addSphere?()
71 | }
72 | AugmentedRealityButton(image: "capsule.righthalf.filled", text: "Sprinkler") {
73 | model.addSprinkler?()
74 | }
75 | }
76 | .frame(height: 80)
77 | .padding()
78 | .background(
79 | Color(uiColor: .secondarySystemBackground)
80 | )
81 | }
82 | .onChange(of: focused) { newValue in
83 | model.resume?(newValue)
84 | }
85 | }
86 | }
87 |
88 | struct ARViewControllerRepresentable: UIViewControllerRepresentable {
89 | @ObservedObject var model: ARViewModel
90 |
91 | func makeUIViewController(context _: Context) -> ARViewController {
92 | return ARViewController(model: model)
93 | }
94 |
95 | func updateUIViewController(_: ARViewController, context _: Context) {}
96 | }
97 |
98 | class ARViewController: UIViewController, ARSCNViewDelegate {
99 | var model: ARViewModel
100 | var sceneView: ARSCNView!
101 | var nodes = [SCNNode]()
102 |
103 | let configuration = ARWorldTrackingConfiguration()
104 |
105 | @available(*, unavailable)
106 | public required init?(coder _: NSCoder) {
107 | fatalError("init(coder:) has not been implemented")
108 | }
109 |
110 | init(model: ARViewModel) {
111 | self.model = model
112 | configuration.planeDetection = .horizontal
113 |
114 | super.init(nibName: nil, bundle: nil)
115 |
116 | model.clear = { [weak self] in
117 | guard let self = self else { return }
118 | for node in self.nodes {
119 | node.removeFromParentNode()
120 | }
121 | }
122 |
123 | model.resume = { [weak self] resume in
124 | guard let self = self else { return }
125 | if resume {
126 | self.sceneView.session.run(self.configuration)
127 | } else {
128 | self.sceneView.session.pause()
129 | }
130 | }
131 |
132 | model.addText = { [weak self] in
133 | guard let self = self else { return }
134 | if let position = self.getPosition() {
135 | let textGeometry = SCNText(string: "Coding Club", extrusionDepth: 0.8)
136 | textGeometry.font = UIFont.systemFont(ofSize: 18, weight: .medium)
137 | textGeometry.firstMaterial?.diffuse.contents = UIColor.systemBlue
138 |
139 | let node = SCNNode(geometry: textGeometry)
140 | let lookAtConstraint = SCNBillboardConstraint()
141 | node.constraints = [lookAtConstraint]
142 | node.position = SCNVector3(
143 | x: position.x,
144 | y: position.y,
145 | z: position.z
146 | )
147 | node.scale = SCNVector3(0.006, 0.006, 0.006)
148 | self.nodes.append(node)
149 | self.sceneView.scene.rootNode.addChildNode(node)
150 | }
151 | }
152 |
153 | model.addSphere = { [weak self] in
154 | guard let self = self else { return }
155 | if let position = self.getPosition() {
156 | let sphereGeometry = SCNSphere(radius: 0.06)
157 | sphereGeometry.firstMaterial?.diffuse.contents = UIColor.green
158 |
159 | let node = SCNNode(geometry: sphereGeometry)
160 | node.position = SCNVector3(
161 | x: position.x,
162 | y: position.y,
163 | z: position.z
164 | )
165 | self.nodes.append(node)
166 | self.sceneView.scene.rootNode.addChildNode(node)
167 | }
168 | }
169 | model.addSprinkler = { [weak self] in
170 | guard let self = self else { return }
171 | if let position = self.getPosition() {
172 | let cylinderGeometry = SCNCylinder(radius: 0.06, height: 0.1)
173 | cylinderGeometry.firstMaterial?.diffuse.contents = UIColor.gray
174 |
175 | let cylinderNode = SCNNode(geometry: cylinderGeometry)
176 | cylinderNode.position = SCNVector3(
177 | x: position.x,
178 | y: position.y,
179 | z: position.z
180 | )
181 |
182 | let particleSystem = SCNParticleSystem()
183 | particleSystem.birthRate = 80
184 | particleSystem.particleSize = 0.01
185 | particleSystem.particleLifeSpan = 2
186 | particleSystem.particleColor = .green
187 | particleSystem.particleVelocity = 5
188 | particleSystem.spreadingAngle = 10
189 | particleSystem.isAffectedByGravity = true
190 | particleSystem.particleColorVariation = SCNVector4(0.25, 0, 0, 0)
191 |
192 | let particlesNode = SCNNode()
193 | particlesNode.position = SCNVector3(
194 | x: position.x,
195 | y: position.y + 0.1,
196 | z: position.z
197 | )
198 | particlesNode.addParticleSystem(particleSystem)
199 | self.nodes.append(cylinderNode)
200 | self.nodes.append(particlesNode)
201 | self.sceneView.scene.rootNode.addChildNode(cylinderNode)
202 | self.sceneView.scene.rootNode.addChildNode(particlesNode)
203 | }
204 | }
205 | }
206 |
207 | var crosshairImageView: UIImageView!
208 |
209 | override func loadView() {
210 | super.loadView()
211 | view = UIView()
212 | view.backgroundColor = .black
213 |
214 | sceneView = ARSCNView()
215 | view.addSubview(sceneView)
216 | sceneView.translatesAutoresizingMaskIntoConstraints = false
217 | NSLayoutConstraint.activate([
218 | sceneView.topAnchor.constraint(equalTo: view.topAnchor),
219 | sceneView.rightAnchor.constraint(equalTo: view.rightAnchor),
220 | sceneView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
221 | sceneView.leftAnchor.constraint(equalTo: view.leftAnchor),
222 | ])
223 |
224 | let configuration = UIImage.SymbolConfiguration(pointSize: 36)
225 | let image = UIImage(systemName: "plus", withConfiguration: configuration)?.withTintColor(.white, renderingMode: .alwaysOriginal)
226 | crosshairImageView = UIImageView(image: image)
227 | view.addSubview(crosshairImageView)
228 | crosshairImageView.translatesAutoresizingMaskIntoConstraints = false
229 | NSLayoutConstraint.activate([
230 | crosshairImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
231 | crosshairImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
232 | ])
233 |
234 | sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
235 | }
236 |
237 | override func viewWillAppear(_ animated: Bool) {
238 | super.viewWillAppear(animated)
239 |
240 | sceneView.autoenablesDefaultLighting = true
241 | sceneView.session.run(configuration)
242 | }
243 |
244 | func getPosition() -> simd_float4? {
245 | let touchLocation = crosshairImageView.center
246 | let results = sceneView.hitTest(touchLocation, types: .existingPlaneUsingExtent)
247 | if let hitResult = results.first {
248 | let position = hitResult.worldTransform.columns.3
249 | return position
250 | }
251 | return nil
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/Container.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Container.swift
3 | // ClubDay2022
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/13/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct Container: View {
12 | @ViewBuilder let view: Content
13 |
14 | var body: some View {
15 | view
16 | .background(.white)
17 | .cornerRadius(24)
18 | .padding(10)
19 | .background(.black)
20 | .cornerRadius(30)
21 | .shadow(
22 | color: .black.opacity(0.3),
23 | radius: 20,
24 | x: 2,
25 | y: 5
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ContentView: View {
4 | @State var index = 0
5 | @State var bubbles = [Bubble]()
6 | @State var selectedItem = 0
7 | @State var sessionID = UUID()
8 | @State var hueRotation = false
9 | @State var showingInfo = false
10 |
11 | let offset1 = CGSize(width: -520, height: 0) /// game
12 | let offset2 = CGSize(width: 0, height: -360) /// AR
13 | let offset3 = CGSize(width: 520, height: 0) /// todo
14 | let offset4 = CGSize(width: 0, height: 360) /// tutorial
15 |
16 | var body: some View {
17 | ZStack {
18 | if index >= 7 {
19 | LinearGradient(
20 | colors: [.blue, .green],
21 | startPoint: .topLeading,
22 | endPoint: .bottomTrailing
23 | )
24 | .hueRotation(.degrees(hueRotation ? 180 : 0))
25 | .ignoresSafeArea()
26 | }
27 |
28 | ZStack {
29 | HStack(spacing: 30) {
30 | if index >= 1 {
31 | Image("Logo")
32 | .resizable()
33 | .frame(width: 136, height: 132)
34 | .cornerRadius(32)
35 | .transition(.scale)
36 | }
37 |
38 | if index != 10 && index != 11 {
39 | Text("Coding Club")
40 | .gradientForeground([.blue, Color(uiColor: UIColor(hex: 0x00AEEF))])
41 | .font(.system(size: 136, weight: .semibold))
42 | }
43 | }
44 | .scaleEffect(index >= 2 ? 0.96 : 1)
45 | .padding(index >= 2 ? 60 : 20)
46 | .background {
47 | Color(uiColor: .systemBackground)
48 | .cornerRadius(48)
49 | }
50 | .overlay(
51 | RoundedRectangle(cornerRadius: 48)
52 | .trim(from: 0, to: index >= 3 ? 1 : 0)
53 | .stroke(
54 | AngularGradient(
55 | colors: [
56 | .red,
57 | .yellow,
58 | .green,
59 | .blue,
60 | .red,
61 | ],
62 | center: .center
63 | ),
64 | lineWidth: 10
65 | )
66 | )
67 | .scaleEffect(index == 11 ? 2 : 1)
68 | .frame(maxWidth: .infinity, maxHeight: .infinity)
69 | .background {
70 | BackgroundView(index: index, bubbles: bubbles)
71 | .disabled(selectedItem != 0)
72 | .onTapGesture {
73 | withAnimation(.spring()) {
74 | selectedItem = 0
75 | }
76 | }
77 | }
78 | .transition(.scale(scale: 6).combined(with: .opacity))
79 | .padding(index >= 5 ? 90 : 0)
80 | .scaleEffect(index >= 9 ? 0.8 : 1)
81 | .rotation3DEffect(.degrees(getRotationDegrees()), axis: (x: 0.8, y: 0.4, z: 0.2))
82 | .zIndex(selectedItem == 0 ? 5 : 0)
83 |
84 | Container {
85 | GameView()
86 | }
87 | .frame(width: 500, height: 900)
88 | .disabled(selectedItem != 1)
89 | .onTapGesture {
90 | withAnimation(.spring()) {
91 | selectedItem = 1
92 | }
93 | }
94 | .scaleEffect(selectedItem == 1 && index >= 9 ? 1 : 0.6)
95 | .rotation3DEffect(.degrees(selectedItem == 1 ? 0 : 10), axis: (x: 0.8, y: 0.4, z: 0.2))
96 | .offset(offset1)
97 | .opacity(index >= 9 ? 1 : 0)
98 |
99 | Container {
100 | AugmentedRealityView(focused: selectedItem == 2)
101 | }
102 | .frame(width: 900, height: 700)
103 | .disabled(selectedItem != 2)
104 | .onTapGesture {
105 | withAnimation(.spring()) {
106 | selectedItem = 2
107 | }
108 | }
109 |
110 | .scaleEffect(selectedItem == 2 && index >= 9 ? 1 : 0.35)
111 | .rotation3DEffect(.degrees(selectedItem == 2 ? 0 : 10), axis: (x: -0.8, y: 0.4, z: 0.2))
112 | .offset(offset2)
113 | .opacity(index >= 9 ? 1 : 0)
114 | .zIndex(1)
115 |
116 | Container {
117 | TodoListView()
118 | }
119 |
120 | .disabled(selectedItem != 3)
121 | .onTapGesture {
122 | withAnimation(.spring()) {
123 | selectedItem = 3
124 | }
125 | }
126 | .frame(width: 500, height: 900)
127 | .scaleEffect(selectedItem == 3 && index >= 9 ? 1 : 0.6)
128 | .rotation3DEffect(.degrees(selectedItem == 3 ? 0 : 10), axis: (x: -0.1, y: 0.5, z: -0.3))
129 | .offset(offset3)
130 | .opacity(index >= 9 ? 1 : 0)
131 |
132 | Container {
133 | TutorialView()
134 | }
135 |
136 | .disabled(selectedItem != 4)
137 | .onTapGesture {
138 | withAnimation(.spring()) {
139 | selectedItem = 4
140 | }
141 | }
142 | .frame(width: 900, height: 700)
143 | .scaleEffect(selectedItem == 4 && index >= 9 ? 1 : 0.35)
144 | .rotation3DEffect(.degrees(selectedItem == 4 ? 0 : 10), axis: (x: 0.5, y: 0.6, z: -0.2))
145 | .offset(offset4)
146 | .opacity(index >= 9 ? 1 : 0)
147 | .zIndex(1)
148 | }
149 | .opacity(showingInfo ? 0 : 1)
150 | .offset(entireOffset())
151 |
152 | if showingInfo {
153 | VStack(alignment: .leading) {
154 | Text("Room 316")
155 | .font(.system(size: 180, weight: .semibold))
156 | .foregroundColor(.white)
157 |
158 | Text("Wednesdays")
159 | .font(.system(size: 136, weight: .semibold, design: .rounded))
160 | .foregroundColor(.white)
161 |
162 | Text("@ Lunch")
163 | .font(.system(size: 136, weight: .semibold, design: .rounded))
164 | .foregroundColor(.white)
165 | }
166 | .transition(.scale)
167 | }
168 | }
169 | .ignoresSafeArea()
170 | .overlay(alignment: .bottom) {
171 | HStack {
172 | Spacer()
173 |
174 | Button {
175 | withAnimation(.spring()) {
176 | index = 0
177 | selectedItem = 0
178 | bubbles.removeAll()
179 | hueRotation = false
180 | showingInfo = false
181 | start()
182 | }
183 | } label: {
184 | Image(systemName: "arrow.clockwise")
185 | .foregroundColor(.white)
186 | .font(.largeTitle)
187 | .frame(width: 64, height: 64)
188 | .background(.white.opacity(0.3))
189 | .cornerRadius(32)
190 | .padding()
191 | }
192 | }
193 | .opacity(index >= 9 ? 1 : 0)
194 | }
195 | .onAppear {
196 | start()
197 | }
198 | }
199 |
200 | func entireOffset() -> CGSize {
201 | let offset: CGSize
202 | switch selectedItem {
203 | case 0:
204 | offset = .zero
205 | case 1:
206 | offset = CGSize(width: -offset1.width, height: -offset1.height)
207 | case 2:
208 | offset = CGSize(width: -offset2.width, height: -offset2.height)
209 | case 3:
210 | offset = CGSize(width: -offset3.width, height: -offset3.height)
211 | case 4:
212 | offset = CGSize(width: -offset4.width, height: -offset4.height)
213 | default:
214 | offset = .zero
215 | }
216 |
217 | return offset
218 | }
219 |
220 | func nextStep(_ currentID: UUID, delay: CGFloat, animation: Animation = .spring()) {
221 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
222 | if currentID == sessionID {
223 | withAnimation(animation) {
224 | index += 1
225 | }
226 | }
227 | }
228 | }
229 |
230 | func addBubbles() {
231 | for index in 0 ..< 36 {
232 | DispatchQueue.main.asyncAfter(deadline: .now() + CGFloat(index) / 15) {
233 | addBubble(colorOffset: CGFloat(index) / 18)
234 | }
235 | }
236 |
237 | DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
238 | removeBubbles()
239 | }
240 | }
241 |
242 | func removeBubbles() {
243 | for index in 0 ..< bubbles.count {
244 | DispatchQueue.main.asyncAfter(deadline: .now() + CGFloat(index) / 15) {
245 | if bubbles.count >= 1 {
246 | withAnimation(.spring(
247 | response: 0.8,
248 | dampingFraction: 0.6,
249 | blendDuration: 0.8
250 | )) {
251 | bubbles.removeLast()
252 | }
253 | }
254 | }
255 | }
256 |
257 | DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
258 | addBubbles()
259 | }
260 | }
261 |
262 | func start() {
263 | sessionID = UUID()
264 | let currentID = sessionID
265 |
266 | nextStep(currentID, delay: 1)
267 | nextStep(currentID, delay: 2)
268 | nextStep(currentID, delay: 3, animation: .easeOut(duration: 3))
269 |
270 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
271 | if currentID == sessionID {
272 | index = 4
273 | addBubbles()
274 | }
275 | }
276 |
277 | nextStep(currentID, delay: 5, animation: .spring(
278 | response: 3,
279 | dampingFraction: 0.7,
280 | blendDuration: 0.8
281 | ))
282 |
283 | nextStep(currentID, delay: 6)
284 |
285 | nextStep(currentID, delay: 7, animation: .spring(
286 | response: 3,
287 | dampingFraction: 0.7,
288 | blendDuration: 0.8
289 | ))
290 |
291 | nextStep(currentID, delay: 8, animation: .easeOut(duration: 6))
292 | DispatchQueue.main.asyncAfter(deadline: .now() + 9) {
293 | if currentID == sessionID {
294 | withAnimation(.easeOut(duration: 10).repeatForever(autoreverses: true)) {
295 | hueRotation = true
296 | }
297 | }
298 | }
299 |
300 | nextStep(currentID, delay: 9, animation: .spring(
301 | response: 3,
302 | dampingFraction: 0.7,
303 | blendDuration: 0.8
304 | ))
305 |
306 | /// hide text
307 | nextStep(currentID, delay: 10, animation: .spring(
308 | response: 0.8,
309 | dampingFraction: 0.7,
310 | blendDuration: 0.8
311 | ))
312 |
313 | nextStep(currentID, delay: 11, animation: .spring(
314 | response: 3,
315 | dampingFraction: 0.7,
316 | blendDuration: 0.8
317 | ))
318 |
319 | nextStep(currentID, delay: 17, animation: .spring(
320 | response: 3,
321 | dampingFraction: 0.7,
322 | blendDuration: 0.8
323 | ))
324 | }
325 |
326 | func getRotationDegrees() -> CGFloat {
327 | if index >= 9, selectedItem == 0 {
328 | return 0
329 | } else if index >= 8 {
330 | return 10
331 | }
332 | return 0
333 | }
334 |
335 | func addBubble(colorOffset: CGFloat) {
336 | let uiColor = UIColor.systemBlue.offset(by: colorOffset)
337 |
338 | let piPercent = colorOffset * 2 * .pi
339 |
340 | let x: CGFloat
341 | let y: CGFloat
342 | let alpha: CGFloat
343 |
344 | if colorOffset >= 1 {
345 | x = cos(piPercent) * 340
346 | y = sin(piPercent) * 340
347 | alpha = 0.4
348 | } else {
349 | x = cos(piPercent) * 200
350 | y = sin(piPercent) * 200
351 | alpha = 8
352 | }
353 |
354 | let newBubble = Bubble(
355 | length: 300,
356 | offset: CGSize(
357 | width: x,
358 | height: y
359 | ),
360 | color: uiColor,
361 | opacity: alpha
362 | )
363 |
364 | withAnimation(.spring(
365 | response: 0.8,
366 | dampingFraction: 0.6,
367 | blendDuration: 0.8
368 | )) {
369 | bubbles.append(newBubble)
370 | }
371 | }
372 | }
373 |
374 | public extension View {
375 | func gradientForeground(_ colors: [Color]) -> some View {
376 | overlay(
377 | LinearGradient(
378 | colors: colors,
379 | startPoint: .topLeading,
380 | endPoint: .bottomTrailing
381 | )
382 | )
383 | .mask(self)
384 | }
385 | }
386 |
387 | public extension UIColor {
388 | /**
389 | Create a UIColor from a hex code.
390 |
391 | Example:
392 |
393 | let color = UIColor(hex: 0x00aeef)
394 | */
395 | convenience init(hex: UInt, alpha: CGFloat = 1) {
396 | self.init(
397 | red: CGFloat((hex & 0xFF0000) >> 16) / 255.0,
398 | green: CGFloat((hex & 0x00FF00) >> 8) / 255.0,
399 | blue: CGFloat(hex & 0x0000FF) / 255.0,
400 | alpha: alpha
401 | )
402 | }
403 | }
404 |
405 | struct DotsView: View {
406 | let color: Color
407 | let offset: Bool
408 |
409 | var body: some View {
410 | HStack {
411 | ForEach(0 ..< 100, id: \.self) { index in
412 | Circle()
413 | .fill(color)
414 | .frame(width: 10, height: 10)
415 | .offset(x: 0, y: offset ? 0 : -600)
416 | .animation(
417 | .spring(
418 | response: 1,
419 | dampingFraction: 0.1,
420 | blendDuration: 1
421 | )
422 | .delay(Double(index) * 0.08),
423 | value: offset
424 | )
425 | }
426 | }
427 | }
428 | }
429 |
430 | struct Bubble: Identifiable {
431 | let id = UUID()
432 | var length: CGFloat
433 | var offset: CGSize
434 | var color: UIColor
435 | var opacity: CGFloat
436 | }
437 |
438 | /// get a gradient color
439 | extension UIColor {
440 | func offset(by offset: CGFloat) -> UIColor {
441 | let (h, s, b, a) = hsba
442 | var newHue = h - offset
443 |
444 | /// make it go back to positive
445 | while newHue <= 0 {
446 | newHue += 1
447 | }
448 | let normalizedHue = newHue.truncatingRemainder(dividingBy: 1)
449 | return UIColor(hue: normalizedHue, saturation: s, brightness: b, alpha: a)
450 | }
451 |
452 | var hsba: (h: CGFloat, s: CGFloat, b: CGFloat, a: CGFloat) {
453 | var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
454 | self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
455 | return (h: h, s: s, b: b, a: a)
456 | }
457 | }
458 |
459 | struct BackgroundView: View {
460 | let index: Int
461 | let bubbles: [Bubble]
462 |
463 | var body: some View {
464 | if index >= 2 {
465 | Color.black
466 | .cornerRadius(index >= 5 ? 60 : 0)
467 | .shadow(
468 | color: .black.opacity(0.3),
469 | radius: 20,
470 | x: 2,
471 | y: 5
472 | )
473 | .overlay {
474 | ZStack {
475 | ForEach(bubbles) { bubble in
476 | Circle()
477 | .fill(
478 | LinearGradient(
479 | colors: [
480 | Color(uiColor: bubble.color),
481 | Color(uiColor: bubble.color.offset(by: 0.1)),
482 | ],
483 | startPoint: .topLeading,
484 | endPoint: .bottomTrailing
485 | )
486 | )
487 | .opacity(bubble.opacity)
488 | .frame(width: bubble.length, height: bubble.length)
489 | .offset(bubble.offset)
490 | .transition(.scale)
491 | }
492 | }
493 | }
494 | }
495 | }
496 | }
497 |
--------------------------------------------------------------------------------
/Demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AHSCodingClub/ClubDay2022-Spring/fbdc4ce94ece5d7a14c721741e984a7f4ddb7367/Demo.mp4
--------------------------------------------------------------------------------
/GameView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GameView.swift
3 | // SwiftPlaygrounds
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/10/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | enum Player {
12 | case red
13 | case blue
14 |
15 | var color: Color {
16 | switch self {
17 | case .red:
18 | return .red
19 | case .blue:
20 | return .blue
21 | }
22 | }
23 | }
24 |
25 | struct Question {
26 | var text: String
27 | var answer: Int
28 | }
29 |
30 | struct GameView: View {
31 | @AppStorage("targetScore") var targetScore = 5
32 | @State var redScore = 0
33 | @State var blueScore = 0
34 | @State var redQuestion = Question(text: "2 + 2", answer: 4)
35 | @State var blueQuestion = Question(text: "2 + 2", answer: 4)
36 | @State var winner: Player?
37 | @State var currentGameID = UUID()
38 |
39 | var body: some View {
40 | VStack {
41 | GameSideView(player: .red, targetScore: targetScore, score: $redScore, question: $redQuestion, winner: $winner, currentGameID: $currentGameID)
42 | .scaleEffect(x: -1, y: -1)
43 |
44 | GameSideView(player: .blue, targetScore: targetScore, score: $blueScore, question: $blueQuestion, winner: $winner, currentGameID: $currentGameID)
45 | }
46 | .overlay {
47 | CenterView(
48 | targetScore: $targetScore,
49 | redScore: $redScore,
50 | blueScore: $blueScore,
51 | redQuestion: $redQuestion,
52 | blueQuestion: $blueQuestion
53 | )
54 | .offset(getCenterViewOffset())
55 | }
56 | .overlay {
57 | VStack {
58 | VStack {
59 | if let winner = winner {
60 | switch winner {
61 | case .red:
62 | Text("Red wins!")
63 | .foregroundColor(.red)
64 | .bold()
65 | Text("Score: \(redScore)")
66 | case .blue:
67 | Text("Blue wins!")
68 | .foregroundColor(.blue)
69 | .bold()
70 | Text("Score: \(blueScore)")
71 | }
72 | }
73 | }
74 |
75 | Button {
76 | redScore = 0
77 | blueScore = 0
78 | redQuestion = Question(text: "2 + 2", answer: 4)
79 | blueQuestion = Question(text: "2 + 2", answer: 4)
80 | currentGameID = UUID()
81 |
82 | withAnimation(.spring()) {
83 | winner = nil
84 | }
85 |
86 | } label: {
87 | Text("Play Again?")
88 | }
89 | .font(.title)
90 | .foregroundColor(.white)
91 | .padding()
92 | .background(.green)
93 | .cornerRadius(12)
94 | }
95 | .font(.largeTitle)
96 | .padding()
97 | .frame(maxWidth: 300, maxHeight: 300)
98 | .background(.ultraThickMaterial)
99 | .cornerRadius(16)
100 | .offset(x: 0, y: winner == nil ? 1000 : 0)
101 | .opacity(winner == nil ? 0 : 1)
102 | }
103 | }
104 |
105 | func getCenterViewOffset() -> CGSize {
106 | let difference = redScore - blueScore
107 | let offset = difference * 10
108 |
109 | if let winner = winner {
110 | switch winner {
111 | case .red:
112 | return CGSize(width: 0, height: 600)
113 | case .blue:
114 | return CGSize(width: 0, height: -600)
115 | }
116 | } else {
117 | return CGSize(width: 0, height: offset)
118 | }
119 | }
120 | }
121 |
122 | struct CenterView: View {
123 | @Binding var targetScore: Int
124 | @Binding var redScore: Int
125 | @Binding var blueScore: Int
126 | @Binding var redQuestion: Question
127 | @Binding var blueQuestion: Question
128 | @State var showingSettings = false
129 |
130 | var body: some View {
131 | ZStack {
132 | HStack {
133 | VStack(spacing: 16) {
134 | Text("Score: \(redScore)")
135 | .foregroundColor(.red)
136 | .scaleEffect(x: -1, y: -1)
137 |
138 | Text("Score: \(blueScore)")
139 | .foregroundColor(.blue)
140 | }
141 | .padding()
142 | Spacer()
143 | }
144 | .background(
145 | Rectangle()
146 | .fill(.green)
147 | .frame(height: 5)
148 | )
149 | .overlay(
150 | HStack {
151 | Spacer()
152 |
153 | Button {
154 | withAnimation {
155 | showingSettings.toggle()
156 | }
157 | } label: {
158 | VStack {
159 | if showingSettings {
160 | Image(systemName: "xmark")
161 | } else {
162 | Image(systemName: "gearshape.fill")
163 | }
164 | }
165 | .font(.title2.bold())
166 | .foregroundColor(.green)
167 | .frame(width: 40, height: 40)
168 | .overlay(
169 | Circle()
170 | .stroke(Color.green, lineWidth: 5)
171 | )
172 | }
173 | .background(Color(uiColor: .systemBackground))
174 | .cornerRadius(20)
175 | .padding(.trailing, 10)
176 | }
177 | )
178 |
179 | VStack(spacing: 0) {
180 | QuestionSideView(question: $redQuestion)
181 | .scaleEffect(x: -1, y: -1)
182 | .padding()
183 |
184 | Rectangle()
185 | .fill(.green)
186 | .opacity(0.5)
187 | .frame(height: 1)
188 |
189 | QuestionSideView(question: $blueQuestion)
190 | .padding()
191 | }
192 | .frame(maxWidth: 150)
193 | .background(Color(uiColor: .systemBackground))
194 | .cornerRadius(16)
195 | .overlay(
196 | RoundedRectangle(cornerRadius: 16)
197 | .stroke(Color.green, lineWidth: 5)
198 | )
199 | }
200 |
201 | .overlay(
202 | VStack {
203 | Text("Change Target Score (\(targetScore))")
204 |
205 | HStack {
206 | Group {
207 | Button("5") { withAnimation { showingSettings = false }; targetScore = 5 }
208 | Button("10") { withAnimation { showingSettings = false }; targetScore = 10 }
209 | Button("20") { withAnimation { showingSettings = false }; targetScore = 20 }
210 | }
211 | .padding()
212 | .background(Color(uiColor: .systemBackground))
213 | .cornerRadius(12)
214 | }
215 | }
216 | .padding()
217 | .background(.regularMaterial)
218 | .cornerRadius(16)
219 | .opacity(showingSettings ? 1 : 0)
220 | .offset(x: 0, y: showingSettings ? 0 : 80)
221 | )
222 | }
223 | }
224 |
225 | struct QuestionSideView: View {
226 | @Binding var question: Question
227 |
228 | var body: some View {
229 | Text(question.text)
230 | .font(.system(.largeTitle, design: .rounded))
231 | .fontWeight(.semibold)
232 | }
233 | }
234 |
235 | struct GameButton: View {
236 | let color: Color
237 | let number: Int
238 | let action: () -> Void
239 |
240 | var body: some View {
241 | Button(action: action) {
242 | Text("\(number)")
243 | .font(.title)
244 | .bold()
245 | .foregroundColor(color)
246 | .frame(maxWidth: .infinity, maxHeight: .infinity)
247 | .background(.ultraThinMaterial)
248 | .cornerRadius(16)
249 | }
250 | }
251 | }
252 |
253 | struct GameSideView: View {
254 | let player: Player
255 | let targetScore: Int
256 | @Binding var score: Int
257 | @Binding var question: Question
258 | @State var bubbles = [Bubble]()
259 | @Binding var winner: Player?
260 | @Binding var currentGameID: UUID
261 |
262 | var body: some View {
263 | VStack {
264 | HStack {
265 | GameButton(color: player.color, number: 1) { validate(attemptedAnswer: 1) }
266 | GameButton(color: player.color, number: 2) { validate(attemptedAnswer: 2) }
267 | GameButton(color: player.color, number: 3) { validate(attemptedAnswer: 3) }
268 | }
269 | HStack {
270 | GameButton(color: player.color, number: 4) { validate(attemptedAnswer: 4) }
271 | GameButton(color: player.color, number: 5) { validate(attemptedAnswer: 5) }
272 | GameButton(color: player.color, number: 6) { validate(attemptedAnswer: 6) }
273 | }
274 | HStack {
275 | GameButton(color: player.color, number: 7) { validate(attemptedAnswer: 7) }
276 | GameButton(color: player.color, number: 8) { validate(attemptedAnswer: 8) }
277 | GameButton(color: player.color, number: 9) { validate(attemptedAnswer: 9) }
278 | }
279 | }
280 | .padding()
281 | .padding(.top, 150)
282 | .background {
283 | ZStack {
284 | ForEach(bubbles) { bubble in
285 | Circle()
286 | .fill(
287 | LinearGradient(
288 | colors: [
289 | Color(uiColor: bubble.color),
290 | Color(uiColor: bubble.color.offset(by: 0.1)),
291 | ],
292 | startPoint: .topLeading,
293 | endPoint: .bottomTrailing
294 | )
295 | )
296 | .opacity(bubble.opacity)
297 | .frame(width: bubble.length, height: bubble.length)
298 | .offset(bubble.offset)
299 | .transition(.scale)
300 | }
301 | }
302 | }
303 | .onChange(of: winner) { _ in
304 | if let winner = winner {
305 | let currentID = currentGameID
306 | for index in 0 ..< 60 {
307 | DispatchQueue.main.asyncAfter(deadline: .now() + CGFloat(index) / 15) {
308 | if currentID == currentGameID {
309 | let color = winner == .blue ? UIColor.systemBlue : UIColor.systemRed
310 | let colorIndex = index - 30
311 | addBubble(colorOffset: CGFloat(colorIndex) / 300, overrideColor: color)
312 | }
313 | }
314 | }
315 | } else {
316 | withAnimation {
317 | bubbles = []
318 | }
319 | }
320 | }
321 | }
322 |
323 | func validate(attemptedAnswer: Int) {
324 | if question.answer == attemptedAnswer {
325 | withAnimation(.spring()) {
326 | score += 1
327 | }
328 |
329 | addBubble(colorOffset: CGFloat(score) / 18)
330 | } else {
331 | withAnimation(.spring()) {
332 | if score >= 1 {
333 | score -= 1
334 | }
335 | }
336 |
337 | removeLastBubble()
338 | }
339 |
340 | if score >= targetScore {
341 | withAnimation(.spring()) {
342 | winner = player
343 | }
344 | } else {
345 | question = getQuestion()
346 | }
347 | }
348 |
349 | func getQuestion() -> Question {
350 | let isAddition = Bool.random()
351 | if isAddition {
352 | let firstNumber = Int.random(in: 1 ... 4)
353 | let secondNumber = Int.random(in: 0 ... (9 - firstNumber))
354 |
355 | let text = "\(firstNumber) + \(secondNumber)"
356 | let answer = firstNumber + secondNumber
357 | return Question(text: text, answer: answer)
358 | } else {
359 | let firstNumber = Int.random(in: 5 ... 9)
360 | let secondNumber = Int.random(in: 0 ... (firstNumber - 1))
361 |
362 | let text = "\(firstNumber) - \(secondNumber)"
363 | let answer = firstNumber - secondNumber
364 | return Question(text: text, answer: answer)
365 | }
366 | }
367 |
368 | func addBubble(colorOffset: CGFloat, overrideColor: UIColor? = nil) {
369 | let uiColor: UIColor
370 | if let overrideColor = overrideColor {
371 | uiColor = overrideColor.offset(by: colorOffset)
372 | } else {
373 | uiColor = UIColor(player.color).offset(by: colorOffset)
374 | }
375 | let newBubble = Bubble(
376 | length: CGFloat.random(in: 100 ... 300),
377 | offset: CGSize(
378 | width: CGFloat.random(in: -240 ... 240),
379 | height: CGFloat.random(in: -240 ... 240)
380 | ),
381 | color: uiColor,
382 | opacity: 1
383 | )
384 |
385 | withAnimation(.spring(
386 | response: 0.8,
387 | dampingFraction: 0.6,
388 | blendDuration: 0.8
389 | )) {
390 | bubbles.append(newBubble)
391 | }
392 | withAnimation(.easeOut(duration: 10)) {
393 | if let bubbleIndex = bubbles.firstIndex(where: { $0.id == newBubble.id }) {
394 | bubbles[bubbleIndex].opacity = 0.2
395 | }
396 | }
397 | }
398 |
399 | func removeLastBubble() {
400 | withAnimation(.spring(
401 | response: 0.8,
402 | dampingFraction: 0.6,
403 | blendDuration: 0.8
404 | )) {
405 | if bubbles.count >= 1 {
406 | bubbles.removeLast()
407 | }
408 | }
409 | }
410 | }
411 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 A. Zheng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MyApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct MyApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 |
3 | // WARNING:
4 | // This file is automatically generated.
5 | // Do not edit it by hand because the contents will be replaced.
6 |
7 | import PackageDescription
8 | import AppleProductTypes
9 |
10 | let package = Package(
11 | name: "ClubDay2022",
12 | platforms: [
13 | .iOS("15.2")
14 | ],
15 | products: [
16 | .iOSApplication(
17 | name: "ClubDay2022",
18 | targets: ["AppModule"],
19 | bundleIdentifier: "com.aheze.ClubDay2022",
20 | teamIdentifier: "YA533DMD5J",
21 | displayVersion: "1.0",
22 | bundleVersion: "1",
23 | iconAssetName: "AppIcon",
24 | accentColorAssetName: "AccentColor",
25 | supportedDeviceFamilies: [
26 | .pad,
27 | .phone
28 | ],
29 | supportedInterfaceOrientations: [
30 | .portrait,
31 | .landscapeRight,
32 | .landscapeLeft,
33 | .portraitUpsideDown(.when(deviceFamilies: [.pad]))
34 | ]
35 | )
36 | ],
37 | targets: [
38 | .executableTarget(
39 | name: "AppModule",
40 | path: "."
41 | )
42 | ]
43 | )
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ClubDay2022
2 | Demo app for Club Day showcasing SwiftUI and ARKit
3 |
4 | https://user-images.githubusercontent.com/49819455/149637704-b904bca0-da45-4cb5-8a45-54df9f01de27.mp4
5 |
6 |
--------------------------------------------------------------------------------
/TodoListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoListView.swift
3 | // SwiftPlaygrounds
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/10/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct TodoListItem: Identifiable, Codable {
12 | var id = UUID()
13 | var name = ""
14 | var completed = false
15 | }
16 |
17 | struct PieChart: Shape {
18 | var startAngle: CGFloat
19 | var endAngle: CGFloat
20 |
21 | public var animatableData: AnimatablePair {
22 | get { AnimatablePair(startAngle, endAngle) }
23 | set { (startAngle, endAngle) = (newValue.first, newValue.second) }
24 | }
25 |
26 | func path(in rect: CGRect) -> Path {
27 | var path = Path()
28 | let center = CGPoint(x: rect.midX, y: rect.midY)
29 | path.move(to: center)
30 | path.addArc(
31 | center: center,
32 | radius: rect.height / 2,
33 | startAngle: .radians(startAngle),
34 | endAngle: .radians(endAngle),
35 | clockwise: false
36 | )
37 | return path
38 | }
39 | }
40 |
41 | struct TodoListView: View {
42 | @State var items = [
43 | TodoListItem(name: "Learn Swift", completed: true),
44 | TodoListItem(name: "Learn SwiftUI", completed: false),
45 | TodoListItem(name: "Bake a cookie", completed: false),
46 | TodoListItem(name: "Do HW", completed: false),
47 | ]
48 |
49 | @State var editMode = EditMode.inactive
50 | @State var showingAlert = false
51 |
52 | var body: some View {
53 | NavigationView {
54 | List {
55 | Section("Chart") {
56 | ZStack {
57 | PieChart(startAngle: 0, endAngle: angleCompleted())
58 | .fill(Color.green)
59 |
60 | PieChart(startAngle: angleCompleted(), endAngle: 2 * .pi)
61 | .fill(Color.blue)
62 | }
63 | .frame(height: 150)
64 | .overlay(
65 | Circle()
66 | .fill(Color(uiColor: .systemBackground))
67 | .overlay {
68 | VStack {
69 | let itemsCompletedCount = itemsCompletedCount()
70 | if itemsCompletedCount < items.count {
71 | Text("\(itemsCompletedCount) / \(items.count)")
72 | .font(.system(.largeTitle, design: .rounded).bold())
73 | } else {
74 | Image(systemName: "checkmark")
75 | .font(.system(.largeTitle, design: .rounded).bold())
76 | }
77 | }
78 | .foregroundColor(.green)
79 | }
80 | .padding(24)
81 | )
82 | .padding()
83 | }
84 |
85 | Section("Actions") {
86 | Button {
87 | let newItem = TodoListItem()
88 | withAnimation(.spring()) {
89 | items.insert(newItem, at: 0)
90 | }
91 | } label: {
92 | Text("Add Item")
93 | .foregroundColor(.green)
94 | .frame(maxWidth: .infinity, alignment: .leading)
95 | .contentShape(Rectangle())
96 | }
97 | .buttonStyle(.plain)
98 | .opacity(editMode.isEditing ? 0.5 : 1)
99 |
100 | Button {
101 | showingAlert = true
102 | } label: {
103 | Text("Print Items")
104 | .foregroundColor(.green)
105 | .frame(maxWidth: .infinity, alignment: .leading)
106 | .contentShape(Rectangle())
107 | }
108 | .alert("Here are your current items", isPresented: $showingAlert) {
109 | Button("Ok", role: .cancel) {}
110 | } message: {
111 | let string = items.map { "Name: \($0.name), completed: \($0.completed)" }.joined(separator: "\n")
112 | Text(verbatim: string)
113 | }
114 | .buttonStyle(.plain)
115 | .opacity(editMode.isEditing ? 0.5 : 1)
116 | }
117 |
118 | Section("Items") {
119 | ForEach($items) { $item in
120 | HStack {
121 | TextField("Item Name", text: $item.name)
122 |
123 | Spacer()
124 |
125 | Button {
126 | withAnimation(.easeOut(duration: 0.5)) {
127 | item.completed.toggle()
128 | }
129 | } label: {
130 | Image(systemName: "checkmark.circle")
131 | .symbolVariant(item.completed ? .fill : .none)
132 | .foregroundColor(.green)
133 | }
134 | .buttonStyle(.plain)
135 | }
136 | }
137 | .onDelete(perform: delete)
138 | .onMove(perform: move)
139 | }
140 | }
141 | .listStyle(.insetGrouped)
142 | .navigationTitle("Todo List")
143 | .toolbar {
144 | EditButton()
145 | }
146 | .environment(\.editMode, $editMode)
147 | }
148 | .navigationViewStyle(.stack)
149 | .tint(.green)
150 | }
151 |
152 | func delete(at offsets: IndexSet) {
153 | withAnimation(.spring()) {
154 | items.remove(atOffsets: offsets)
155 | }
156 | }
157 |
158 | func move(from source: IndexSet, to destination: Int) {
159 | items.move(fromOffsets: source, toOffset: destination)
160 | }
161 |
162 | func itemsCompletedCount() -> Int {
163 | let itemsCompleted = items.filter { $0.completed }
164 | return itemsCompleted.count
165 | }
166 |
167 | func angleCompleted() -> CGFloat {
168 | let percentageCompleted = CGFloat(itemsCompletedCount()) / CGFloat(items.count)
169 | let angle = (2 * CGFloat.pi) * percentageCompleted
170 | return angle
171 | }
172 | }
173 |
174 | extension Array: RawRepresentable where Element: Codable {
175 | public init?(rawValue: String) {
176 | guard let data = rawValue.data(using: .utf8),
177 | let result = try? JSONDecoder().decode([Element].self, from: data)
178 | else {
179 | return nil
180 | }
181 | self = result
182 | }
183 |
184 | public var rawValue: String {
185 | guard let data = try? JSONEncoder().encode(self),
186 | let result = String(data: data, encoding: .utf8)
187 | else {
188 | return "[]"
189 | }
190 | return result
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/TutorialView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TutorialView.swift
3 | // ClubDay2022
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/13/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct TutorialView: View {
12 | var body: some View {
13 | HStack {
14 | VStack {
15 | Text("Code")
16 | .font(.system(size: 60, weight: .semibold, design: .monospaced))
17 |
18 | VStack {
19 | Image("TextCode")
20 | .resizable()
21 | .aspectRatio(contentMode: .fit)
22 | Image("ImageCode")
23 | .resizable()
24 | .aspectRatio(contentMode: .fit)
25 | Image("VStackCode")
26 | .resizable()
27 | .aspectRatio(contentMode: .fit)
28 | }
29 |
30 | Spacer()
31 | }
32 | .frame(maxWidth: .infinity, maxHeight: .infinity)
33 | .padding()
34 | .background(Color(uiColor: .secondarySystemFill))
35 | .cornerRadius(24)
36 |
37 | VStack {
38 | Text("Result")
39 | .font(.system(size: 60, weight: .semibold, design: .monospaced))
40 |
41 | VStack {
42 | Text("Hello, world!")
43 | .font(.system(size: 60, weight: .semibold))
44 | .padding(20)
45 | .frame(maxWidth: .infinity)
46 | .background(Color.white)
47 |
48 | Image(systemName: "star")
49 | .font(.system(size: 60, weight: .semibold))
50 | .padding(20)
51 | .frame(maxWidth: .infinity)
52 | .background(Color.white)
53 |
54 | VStack {
55 | Text("Hello, world!")
56 | .font(.system(size: 60, weight: .semibold))
57 |
58 | Image(systemName: "star")
59 | .font(.system(size: 60, weight: .semibold))
60 | }
61 | .padding(20)
62 | .frame(maxWidth: .infinity)
63 | .background(Color.white)
64 | }
65 |
66 | Spacer()
67 | }
68 | .frame(maxWidth: .infinity, maxHeight: .infinity)
69 | .padding()
70 | .background(Color(uiColor: .secondarySystemFill))
71 | .cornerRadius(24)
72 | }
73 | .padding()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------