├── .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 | --------------------------------------------------------------------------------