diff --git a/Download/README.md b/Download/README.md new file mode 100644 index 0000000..cff1091 --- /dev/null +++ b/Download/README.md @@ -0,0 +1,34 @@ +# README + +You can also download the source code project on [https://github.com/JustinFincher/WWDC20Playground](https://github.com/JustinFincher/WWDC20Playground) + +# Contact + +Haotian Zheng +[mailto:justzht@gmail.com](justzht@gmail.com) ++86 18556572637 / +1 4697512468 + +# Tell us about the features and technologies you used in your Swift playground. + +Shader Node Editor is a node-based shader editor framework I wrote for easy graphics programming on iPad (and Mac via Catalyst). The core of Shader Node Editor uses UIKit & UIKit Dynamics for layout and SpriteKit for shader preview & compilation, while the peripheral includes certain usages of AVFoundation, Combine, and SwiftUI. + +Features: +- Visual scripting with a node-based editor +- Capability to easily extend the existing set of nodes even in runtime with subclassing +- Automatic shader preview updates +- Rich uniform input including audio, timestamp, and UV. +- Node-based UI framework developed for generic purposes that can be converted for storyline designer or state machine editor. + +Technologies: +- UIKit Dynamics: the whole canvas along with various nodes follows physical rules in interactions thanks to UIKit Dynamics. You can drag & drop and they will maintain momentum until hitting the boundary. +- Node Graph: as SpriteKit already handles shader compilation for me, my editor only needs to do the code generation part. To generate OpenGL ES code, I deployed a 2-pass-brutal-force-approach. It is far from optimal, but at least it works: The first pass is responsible to use graph searching algorithms to gather linkage information and thus build a dependency graph for each node, and the second pass would declare variables for each knot on the nodes, append equal expressions on linked knots, and finally generate the shader code following the order previously collected in the dependency graph. +- Uniforms: shader uniforms like audio loudness are provided with Singletons for their sole purposes. Currently, the uniform value would be updated in a timer callback, but it can also be adjusted to follow SpriteKit drawing callbacks. + +# Apps on the App Store (optional) + +Developer page (with all apps included): https://itunes.apple.com/cn/developer/haotian-zheng/id981803173?mt=8 + +- Contributions For GitHub (https://itunes.apple.com/cn/app/contributions-for-github/id1153432612?mt=8) A small app for viewing your GitHub contributions graph in 2D / 3D perspective. Available on iOS and watchOS. +- Epoch Core (https://itunes.apple.com/cn/app/epoch-core/id1177530091?mt=8) Tech demo for showing off my noise shader and procedurally generated planet terrain. It can generate near 1 million different planets. +- ArtWall (https://itunes.apple.com/cn/app/artwall/id1178151992?mt=12) If you are a digital artist or just a person who likes digital art, ArtWall is here for you to save ArtStation images as your desktop wallpaper. +- Golf GO (https://itunes.apple.com/cn/app/golf-go-scholarship-edition/id1380656648?mt=8) WWDC 18 winner project, a mini-golf game with infinite golf maps to play. Written in 1000 Swift lines. \ No newline at end of file diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift b/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift index d569725..9970bd6 100644 --- a/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift +++ b/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift @@ -38,6 +38,41 @@ struct ContentView: View { Text("Allow Audio Permission") }.disabled(store.audioPermissionEnabled) } + Section(header: Text("Tutorial: How to connect nodes")) { + VStack(alignment: .leading){ + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial1.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("1. Drag the knot on any side of a node and a line would appear following your position").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial2.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("2. The line would display in red if it cannot connect (no knot or invalid data format)").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial3.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("3. The line would display in green if target knot is compatible").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial4.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("4. Lift your finger when the line is green and it would be added between two knots").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial5.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("5. Drag an already connected knot would make the line disconnected").font(.footnote) + } + } + + } } .listStyle(GroupedListStyle()) .environment(\.horizontalSizeClass, .regular) diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift b/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift index 6254884..df8baa8 100644 --- a/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift +++ b/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift @@ -14,8 +14,12 @@ //: //: You are probably using Shaders all the time but you didn't notice them when you were applying filters on Instagram or playing video games on your iPad. What's more, Shaders are now used in Machine Learning to accelerate the learning process (compute shaders, specifically), and all these are achieved with only pure mathematical expressions, what marvelous engineering! //: -//: --- +//: However, shader might be hard to learn at first due to its parallel computing nature. The program you wrote would be executed thousands of times in one frame with different output values, which is an abstract concept and people sometimes don't get it. That's why Shader Node Editor comes to rescue. With a node-based user interface, you can compose shaders in both feedback-rich and visual-pleasing way, and it is always real-time so you don't need to wait for a debug build. //: //: Without further due, let's dive into Shader programming with Shader Node Editor, my node-based expression editor written in Swift (`UIKit Dynamics`, `SwiftUI`, `SpriteKit`)! //: -//: > ➡️ Please switch to the next page after the reading (and **necessary steps in the playground**). +//: > ➡️ Please switch to the next page after your reading (and **necessary steps in the playground**). Also, for more technicial info, please refer to images below: +//: +//: ![](Tech1.jpg) +//: ![](Tech2.jpg) +//: ![](Tech3.jpg) diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift b/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift index 7bd15b8..1895366 100644 --- a/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift +++ b/Download/ShaderNodeEditor.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift @@ -2,6 +2,7 @@ //#-end-hidden-code //: # 🧑‍💻 Video Jockey //: VJ is like a DJ, but with visuals. Shaders are especially well-fit for such scenarios, let's make an audio-reactive shader that changes with music loudness! 🎶 +//: //: ![](AudioShader.jpg) //: //: **Follow these steps:** diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial1.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial1.jpg new file mode 100644 index 0000000..873abac Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial1.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial2.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial2.jpg new file mode 100644 index 0000000..b67e57a Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial2.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial3.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial3.jpg new file mode 100644 index 0000000..3826740 Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial3.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial4.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial4.jpg new file mode 100644 index 0000000..469a1bf Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial4.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial5.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial5.jpg new file mode 100644 index 0000000..cd44fbd Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/NodeTutorial5.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech1.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech1.jpg new file mode 100644 index 0000000..bd57eb9 Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech1.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech2.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech2.jpg new file mode 100644 index 0000000..76944d2 Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech2.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech3.jpg b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech3.jpg new file mode 100644 index 0000000..e68a6aa Binary files /dev/null and b/Download/ShaderNodeEditor.playgroundbook/Contents/PrivateResources/Tech3.jpg differ diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/Constant.swift b/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/Constant.swift index 2d25a5d..1255566 100644 --- a/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/Constant.swift +++ b/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/Constant.swift @@ -12,6 +12,7 @@ public class Constant: NSObject public static let nodeConnectionCurveControlOffset : CGFloat = 90 public static let fontObliqueName = "Avenir-Oblique" public static let fontBoldName = "Avenir-Black" + public static let tutorialRowHeight : CGFloat = 128 public static let nodeKnotIndicatorColor = UIColor.tertiarySystemFill.withAlphaComponent(0.4) public static let lineNormalColor = UIColor.systemYellow.withAlphaComponent(0.6) public static let lineRejectColor = UIColor.systemRed.withAlphaComponent(0.6) diff --git a/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift b/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift index 884c433..589c396 100644 --- a/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift +++ b/Download/ShaderNodeEditor.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift @@ -6,14 +6,146 @@ // import Foundation +import UIKit import PlaygroundSupport open class LiveViewController : NodeEditorViewController, PlaygroundLiveViewMessageHandler, PlaygroundLiveViewSafeAreaContainer { + public override func viewDidLoad() { + super.viewDidLoad() + + self.navigationItem.prompt = "Long press on canvas to add nodes. Use Cheat Sheet if you need help" + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "🎲 Cheat Sheet", style: .plain, target: self, action: #selector(showCheatSheet)) + } + + @objc func showCheatSheet() -> Void { + let alertController = UIAlertController(title: "Cheat Sheet", message: "Select pre-made node graph if you cannot complete corresponding tutorials but want to see the final result instantly", preferredStyle: .actionSheet) + + alertController.addAction(UIAlertAction(title: "Clear > Reset Graph", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute: + { + self.nodeEditorData.forceUpdate() + }) + })) + + alertController.addAction(UIAlertAction(title: "Complete > Getting Started", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + let floatGeneratorNode1 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode1.frame = CGRect.init(x: 16, y: 16, width: floatGeneratorNode1.frame.size.width, height: floatGeneratorNode1.frame.size.height) + floatGeneratorNode1.value.value = 0.5 + self.nodeEditorData.addNode(node: floatGeneratorNode1) + + let floatGeneratorNode2 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode2.frame = CGRect.init(x: 16, y: 300, width: floatGeneratorNode2.frame.size.width, height: floatGeneratorNode2.frame.size.height) + floatGeneratorNode2.value.value = 0.5 + self.nodeEditorData.addNode(node: floatGeneratorNode2) + + let floatAddNode : FloatAddNodeData = FloatAddNodeData() + floatAddNode.frame = CGRect.init(x: 300, y: 30, width: floatAddNode.frame.size.width, height: floatAddNode.frame.size.height) + self.nodeEditorData.addNode(node: floatAddNode) + + self.nodeEditorData.connectNode(outPort: floatGeneratorNode1.outPorts[0], inPort: floatAddNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode2.outPorts[0], inPort: floatAddNode.inPorts[1]) + self.nodeEditorData.forceUpdate() + })) + + alertController.addAction(UIAlertAction(title: "Complete > Advanced Usages", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + let uvNode : Vec2TexCoordNodeData = Vec2TexCoordNodeData() + uvNode.frame = CGRect.init(x: 16, y: 16, width: uvNode.frame.size.width, height: uvNode.frame.size.height) + self.nodeEditorData.addNode(node: uvNode) + + let uvSplitNode : Vec2ChannelSplitNodeData = Vec2ChannelSplitNodeData() + uvSplitNode.frame = CGRect.init(x: 250, y: 16, width: uvSplitNode.frame.size.width, height: uvSplitNode.frame.size.height) + self.nodeEditorData.addNode(node: uvSplitNode) + self.nodeEditorData.connectNode(outPort: uvNode.outPorts[0], inPort: uvSplitNode.inPorts[0]) + + let timeNode : FloatTimeNodeData = FloatTimeNodeData() + timeNode.frame = CGRect.init(x: 16, y: 300, width: timeNode.frame.size.width, height: timeNode.frame.size.height) + self.nodeEditorData.addNode(node: timeNode) + + let sinNode : FloatSinNodeData = FloatSinNodeData() + sinNode.frame = CGRect.init(x: 250, y: 200, width: sinNode.frame.size.width, height: sinNode.frame.size.height) + self.nodeEditorData.addNode(node: sinNode) + self.nodeEditorData.connectNode(outPort: timeNode.outPorts[0], inPort: sinNode.inPorts[0]) + + let vec4CombineNode : Vec4ChannelCombineNodeData = Vec4ChannelCombineNodeData() + vec4CombineNode.frame = CGRect.init(x: 550, y: 70, width: vec4CombineNode.frame.size.width, height: vec4CombineNode.frame.size.height) + self.nodeEditorData.addNode(node: vec4CombineNode) + self.nodeEditorData.connectNode(outPort: uvSplitNode.outPorts[0], inPort: vec4CombineNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: uvSplitNode.outPorts[1], inPort: vec4CombineNode.inPorts[1]) + self.nodeEditorData.connectNode(outPort: sinNode.outPorts[0], inPort: vec4CombineNode.inPorts[2]) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute: + { + self.nodeEditorData.forceUpdate() + }) + })) + + alertController.addAction(UIAlertAction(title: "Complete > VJ Machine", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + + let atanNode : FloatUVCenterAtanNodeData = FloatUVCenterAtanNodeData() + atanNode.frame = CGRect.init(x: 16, y: 16, width: atanNode.frame.size.width, height: atanNode.frame.size.height) + self.nodeEditorData.addNode(node: atanNode) + + let audioNode : FloatAudioDBNodeData = FloatAudioDBNodeData() + audioNode.frame = CGRect.init(x: 16, y: 300, width: audioNode.frame.size.width, height: audioNode.frame.size.height) + self.nodeEditorData.addNode(node: audioNode) + + let floatGeneratorNode1 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode1.frame = CGRect.init(x: 16, y: 400, width: floatGeneratorNode1.frame.size.width, height: floatGeneratorNode1.frame.size.height) + floatGeneratorNode1.value.value = 5.0 + self.nodeEditorData.addNode(node: floatGeneratorNode1) + + let floatGeneratorNode2 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode2.frame = CGRect.init(x: 16, y: 640, width: floatGeneratorNode2.frame.size.width, height: floatGeneratorNode2.frame.size.height) + floatGeneratorNode2.value.value = 8.0 + self.nodeEditorData.addNode(node: floatGeneratorNode2) + + let discRayNode : FloatRayDiscNodeData = FloatRayDiscNodeData() + discRayNode.frame = CGRect.init(x: 300, y: 60, width: discRayNode.frame.size.width, height: discRayNode.frame.size.height) + self.nodeEditorData.addNode(node: discRayNode) + self.nodeEditorData.connectNode(outPort: atanNode.outPorts[0], inPort: discRayNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: audioNode.outPorts[0], inPort: discRayNode.inPorts[1]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode1.outPorts[0], inPort: discRayNode.inPorts[2]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode2.outPorts[0], inPort: discRayNode.inPorts[3]) + + let floatGeneratorNode3 : FloatSliderGeneratorNodeData = FloatSliderGeneratorNodeData() + floatGeneratorNode3.frame = CGRect.init(x: 320, y: 470, width: floatGeneratorNode3.frame.size.width, height: floatGeneratorNode3.frame.size.height) + floatGeneratorNode3.value.value = 0.0 + self.nodeEditorData.addNode(node: floatGeneratorNode3) + + let floatGeneratorNode4 : FloatSliderGeneratorNodeData = FloatSliderGeneratorNodeData() + floatGeneratorNode4.frame = CGRect.init(x: 370, y: 650, width: floatGeneratorNode4.frame.size.width, height: floatGeneratorNode4.frame.size.height) + floatGeneratorNode4.value.value = 0.3 + self.nodeEditorData.addNode(node: floatGeneratorNode4) + + let circleOutlineNode : FloatCircleOutlineNodeData = FloatCircleOutlineNodeData() + circleOutlineNode.frame = CGRect.init(x: 550, y: 70, width: circleOutlineNode.frame.size.width, height: circleOutlineNode.frame.size.height) + self.nodeEditorData.addNode(node: circleOutlineNode) + + self.nodeEditorData.connectNode(outPort: discRayNode.outPorts[0], inPort: circleOutlineNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode3.outPorts[0], inPort: circleOutlineNode.inPorts[1]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode4.outPorts[0], inPort: circleOutlineNode.inPorts[2]) + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute: + { + self.nodeEditorData.forceUpdate() + }) + })) + + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (action) in + + })) + alertController.popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem + present(alertController, animated: true, completion: {}) + } //MARK: PlaygroundLiveViewMessageHandler public func receive(_ message: PlaygroundValue) { + // guard case let .string(messageString) = message else { return } } public func send(_ message: PlaygroundValue) { diff --git a/PlaygroundBook.xcodeproj/project.pbxproj b/PlaygroundBook.xcodeproj/project.pbxproj index 67a1a25..0194573 100644 --- a/PlaygroundBook.xcodeproj/project.pbxproj +++ b/PlaygroundBook.xcodeproj/project.pbxproj @@ -76,6 +76,14 @@ 0D5F84EB246B683B00050D49 /* FloatSliderGeneratorNodeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5F84EA246B683B00050D49 /* FloatSliderGeneratorNodeData.swift */; }; 0D5F84ED246B703500050D49 /* AudioShader.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0D5F84EC246B703500050D49 /* AudioShader.jpg */; }; 0DB9E24C246BBD4100897A36 /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB9E24B246BBD4100897A36 /* Binding.swift */; }; + 0DB9E252246BEB6900897A36 /* NodeTutorial4.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E24D246BEB6800897A36 /* NodeTutorial4.jpg */; }; + 0DB9E253246BEB6900897A36 /* NodeTutorial5.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E24E246BEB6800897A36 /* NodeTutorial5.jpg */; }; + 0DB9E254246BEB6900897A36 /* NodeTutorial2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E24F246BEB6900897A36 /* NodeTutorial2.jpg */; }; + 0DB9E255246BEB6900897A36 /* NodeTutorial1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E250246BEB6900897A36 /* NodeTutorial1.jpg */; }; + 0DB9E256246BEB6900897A36 /* NodeTutorial3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E251246BEB6900897A36 /* NodeTutorial3.jpg */; }; + 0DB9E25A246BED8E00897A36 /* Tech1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E257246BED8E00897A36 /* Tech1.jpg */; }; + 0DB9E25B246BED8E00897A36 /* Tech2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E258246BED8E00897A36 /* Tech2.jpg */; }; + 0DB9E25C246BED8E00897A36 /* Tech3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0DB9E259246BED8E00897A36 /* Tech3.jpg */; }; 5E086DD32051E3A3004D8D25 /* Manifest.plist in Copy Book Contents */ = {isa = PBXBuildFile; fileRef = 5E086DCF2051DF0F004D8D25 /* Manifest.plist */; }; 5E551EC52371FC3F00784365 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E551EC42371FC3F00784365 /* Assets.xcassets */; }; 5E6F7E592367B529008CC191 /* UserModules in Copy Book Contents */ = {isa = PBXBuildFile; fileRef = 5E6F7E582367B51E008CC191 /* UserModules */; }; @@ -218,6 +226,14 @@ 0D5F84EA246B683B00050D49 /* FloatSliderGeneratorNodeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatSliderGeneratorNodeData.swift; sourceTree = ""; }; 0D5F84EC246B703500050D49 /* AudioShader.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = AudioShader.jpg; sourceTree = ""; }; 0DB9E24B246BBD4100897A36 /* Binding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binding.swift; sourceTree = ""; }; + 0DB9E24D246BEB6800897A36 /* NodeTutorial4.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NodeTutorial4.jpg; sourceTree = ""; }; + 0DB9E24E246BEB6800897A36 /* NodeTutorial5.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NodeTutorial5.jpg; sourceTree = ""; }; + 0DB9E24F246BEB6900897A36 /* NodeTutorial2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NodeTutorial2.jpg; sourceTree = ""; }; + 0DB9E250246BEB6900897A36 /* NodeTutorial1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NodeTutorial1.jpg; sourceTree = ""; }; + 0DB9E251246BEB6900897A36 /* NodeTutorial3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NodeTutorial3.jpg; sourceTree = ""; }; + 0DB9E257246BED8E00897A36 /* Tech1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Tech1.jpg; sourceTree = ""; }; + 0DB9E258246BED8E00897A36 /* Tech2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Tech2.jpg; sourceTree = ""; }; + 0DB9E259246BED8E00897A36 /* Tech3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Tech3.jpg; sourceTree = ""; }; 5E086DC02051C5A7004D8D25 /* ShaderNodeEditor.playgroundbook */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShaderNodeEditor.playgroundbook; sourceTree = BUILT_PRODUCTS_DIR; }; 5E086DC72051DD03004D8D25 /* BookOverridingBuildSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BookOverridingBuildSettings.xcconfig; sourceTree = ""; }; 5E086DCF2051DF0F004D8D25 /* Manifest.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Manifest.plist; sourceTree = ""; }; @@ -465,6 +481,14 @@ 0D5F8499246B21A900050D49 /* Banner.jpg */, 0D5F84EC246B703500050D49 /* AudioShader.jpg */, 0D5F849A246B21AA00050D49 /* Cover.png */, + 0DB9E257246BED8E00897A36 /* Tech1.jpg */, + 0DB9E258246BED8E00897A36 /* Tech2.jpg */, + 0DB9E259246BED8E00897A36 /* Tech3.jpg */, + 0DB9E250246BEB6900897A36 /* NodeTutorial1.jpg */, + 0DB9E24F246BEB6900897A36 /* NodeTutorial2.jpg */, + 0DB9E251246BEB6900897A36 /* NodeTutorial3.jpg */, + 0DB9E24D246BEB6800897A36 /* NodeTutorial4.jpg */, + 0DB9E24E246BEB6800897A36 /* NodeTutorial5.jpg */, 0D5F849F246B432700050D49 /* UVNode.jpg */, 0D5F84A1246B454A00050D49 /* UVPlusBlue.jpg */, 0D5F84A3246B4A3000050D49 /* UVWithSinAsBlue.jpg */, @@ -588,7 +612,10 @@ buildActionMask = 2147483647; files = ( 0D5F84ED246B703500050D49 /* AudioShader.jpg in Resources */, + 0DB9E255246BEB6900897A36 /* NodeTutorial1.jpg in Resources */, + 0DB9E256246BEB6900897A36 /* NodeTutorial3.jpg in Resources */, 5EF2F97C2054B6E400191409 /* ManifestPlist.strings in Resources */, + 0DB9E252246BEB6900897A36 /* NodeTutorial4.jpg in Resources */, 0D5F84A4246B4A3000050D49 /* UVWithSinAsBlue.jpg in Resources */, 5E551EC52371FC3F00784365 /* Assets.xcassets in Resources */, 0D5F849E246B3D6E00050D49 /* BasicAddOperation.jpg in Resources */, @@ -597,6 +624,11 @@ 0D5F849B246B21AA00050D49 /* Banner.jpg in Resources */, 0D5F84A8246B64E500050D49 /* NodeList.txt in Resources */, 0D5F849C246B21AA00050D49 /* Cover.png in Resources */, + 0DB9E25B246BED8E00897A36 /* Tech2.jpg in Resources */, + 0DB9E25A246BED8E00897A36 /* Tech1.jpg in Resources */, + 0DB9E25C246BED8E00897A36 /* Tech3.jpg in Resources */, + 0DB9E253246BEB6900897A36 /* NodeTutorial5.jpg in Resources */, + 0DB9E254246BEB6900897A36 /* NodeTutorial2.jpg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift b/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift index d569725..9970bd6 100644 --- a/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift +++ b/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/LiveView.swift @@ -38,6 +38,41 @@ struct ContentView: View { Text("Allow Audio Permission") }.disabled(store.audioPermissionEnabled) } + Section(header: Text("Tutorial: How to connect nodes")) { + VStack(alignment: .leading){ + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial1.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("1. Drag the knot on any side of a node and a line would appear following your position").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial2.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("2. The line would display in red if it cannot connect (no knot or invalid data format)").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial3.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("3. The line would display in green if target knot is compatible").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial4.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("4. Lift your finger when the line is green and it would be added between two knots").font(.footnote) + } + HStack() + { + Image(uiImage: UIImage(named:"NodeTutorial5.jpg")!).resizable() + .frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight) + Text("5. Drag an already connected knot would make the line disconnected").font(.footnote) + } + } + + } } .listStyle(GroupedListStyle()) .environment(\.horizontalSizeClass, .regular) diff --git a/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift b/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift index 6254884..df8baa8 100644 --- a/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift +++ b/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page1.playgroundpage/main.swift @@ -14,8 +14,12 @@ //: //: You are probably using Shaders all the time but you didn't notice them when you were applying filters on Instagram or playing video games on your iPad. What's more, Shaders are now used in Machine Learning to accelerate the learning process (compute shaders, specifically), and all these are achieved with only pure mathematical expressions, what marvelous engineering! //: -//: --- +//: However, shader might be hard to learn at first due to its parallel computing nature. The program you wrote would be executed thousands of times in one frame with different output values, which is an abstract concept and people sometimes don't get it. That's why Shader Node Editor comes to rescue. With a node-based user interface, you can compose shaders in both feedback-rich and visual-pleasing way, and it is always real-time so you don't need to wait for a debug build. //: //: Without further due, let's dive into Shader programming with Shader Node Editor, my node-based expression editor written in Swift (`UIKit Dynamics`, `SwiftUI`, `SpriteKit`)! //: -//: > ➡️ Please switch to the next page after the reading (and **necessary steps in the playground**). +//: > ➡️ Please switch to the next page after your reading (and **necessary steps in the playground**). Also, for more technicial info, please refer to images below: +//: +//: ![](Tech1.jpg) +//: ![](Tech2.jpg) +//: ![](Tech3.jpg) diff --git a/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift b/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift index 7bd15b8..1895366 100644 --- a/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift +++ b/PlaygroundBook/Chapters/Chapter1.playgroundchapter/Pages/Page4.playgroundpage/main.swift @@ -2,6 +2,7 @@ //#-end-hidden-code //: # 🧑‍💻 Video Jockey //: VJ is like a DJ, but with visuals. Shaders are especially well-fit for such scenarios, let's make an audio-reactive shader that changes with music loudness! 🎶 +//: //: ![](AudioShader.jpg) //: //: **Follow these steps:** diff --git a/PlaygroundBook/PrivateResources/NodeTutorial1.jpg b/PlaygroundBook/PrivateResources/NodeTutorial1.jpg new file mode 100644 index 0000000..873abac Binary files /dev/null and b/PlaygroundBook/PrivateResources/NodeTutorial1.jpg differ diff --git a/PlaygroundBook/PrivateResources/NodeTutorial2.jpg b/PlaygroundBook/PrivateResources/NodeTutorial2.jpg new file mode 100644 index 0000000..b67e57a Binary files /dev/null and b/PlaygroundBook/PrivateResources/NodeTutorial2.jpg differ diff --git a/PlaygroundBook/PrivateResources/NodeTutorial3.jpg b/PlaygroundBook/PrivateResources/NodeTutorial3.jpg new file mode 100644 index 0000000..3826740 Binary files /dev/null and b/PlaygroundBook/PrivateResources/NodeTutorial3.jpg differ diff --git a/PlaygroundBook/PrivateResources/NodeTutorial4.jpg b/PlaygroundBook/PrivateResources/NodeTutorial4.jpg new file mode 100644 index 0000000..469a1bf Binary files /dev/null and b/PlaygroundBook/PrivateResources/NodeTutorial4.jpg differ diff --git a/PlaygroundBook/PrivateResources/NodeTutorial5.jpg b/PlaygroundBook/PrivateResources/NodeTutorial5.jpg new file mode 100644 index 0000000..cd44fbd Binary files /dev/null and b/PlaygroundBook/PrivateResources/NodeTutorial5.jpg differ diff --git a/PlaygroundBook/PrivateResources/Tech1.jpg b/PlaygroundBook/PrivateResources/Tech1.jpg new file mode 100644 index 0000000..bd57eb9 Binary files /dev/null and b/PlaygroundBook/PrivateResources/Tech1.jpg differ diff --git a/PlaygroundBook/PrivateResources/Tech2.jpg b/PlaygroundBook/PrivateResources/Tech2.jpg new file mode 100644 index 0000000..76944d2 Binary files /dev/null and b/PlaygroundBook/PrivateResources/Tech2.jpg differ diff --git a/PlaygroundBook/PrivateResources/Tech3.jpg b/PlaygroundBook/PrivateResources/Tech3.jpg new file mode 100644 index 0000000..e68a6aa Binary files /dev/null and b/PlaygroundBook/PrivateResources/Tech3.jpg differ diff --git a/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/Constant.swift b/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/Constant.swift index 2d25a5d..1255566 100644 --- a/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/Constant.swift +++ b/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/Constant.swift @@ -12,6 +12,7 @@ public class Constant: NSObject public static let nodeConnectionCurveControlOffset : CGFloat = 90 public static let fontObliqueName = "Avenir-Oblique" public static let fontBoldName = "Avenir-Black" + public static let tutorialRowHeight : CGFloat = 128 public static let nodeKnotIndicatorColor = UIColor.tertiarySystemFill.withAlphaComponent(0.4) public static let lineNormalColor = UIColor.systemYellow.withAlphaComponent(0.6) public static let lineRejectColor = UIColor.systemRed.withAlphaComponent(0.6) diff --git a/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift b/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift index 884c433..589c396 100644 --- a/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift +++ b/PlaygroundBook/UserModules/UserModule.playgroundmodule/Sources/LiveViewController.swift @@ -6,14 +6,146 @@ // import Foundation +import UIKit import PlaygroundSupport open class LiveViewController : NodeEditorViewController, PlaygroundLiveViewMessageHandler, PlaygroundLiveViewSafeAreaContainer { + public override func viewDidLoad() { + super.viewDidLoad() + + self.navigationItem.prompt = "Long press on canvas to add nodes. Use Cheat Sheet if you need help" + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "🎲 Cheat Sheet", style: .plain, target: self, action: #selector(showCheatSheet)) + } + + @objc func showCheatSheet() -> Void { + let alertController = UIAlertController(title: "Cheat Sheet", message: "Select pre-made node graph if you cannot complete corresponding tutorials but want to see the final result instantly", preferredStyle: .actionSheet) + + alertController.addAction(UIAlertAction(title: "Clear > Reset Graph", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute: + { + self.nodeEditorData.forceUpdate() + }) + })) + + alertController.addAction(UIAlertAction(title: "Complete > Getting Started", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + let floatGeneratorNode1 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode1.frame = CGRect.init(x: 16, y: 16, width: floatGeneratorNode1.frame.size.width, height: floatGeneratorNode1.frame.size.height) + floatGeneratorNode1.value.value = 0.5 + self.nodeEditorData.addNode(node: floatGeneratorNode1) + + let floatGeneratorNode2 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode2.frame = CGRect.init(x: 16, y: 300, width: floatGeneratorNode2.frame.size.width, height: floatGeneratorNode2.frame.size.height) + floatGeneratorNode2.value.value = 0.5 + self.nodeEditorData.addNode(node: floatGeneratorNode2) + + let floatAddNode : FloatAddNodeData = FloatAddNodeData() + floatAddNode.frame = CGRect.init(x: 300, y: 30, width: floatAddNode.frame.size.width, height: floatAddNode.frame.size.height) + self.nodeEditorData.addNode(node: floatAddNode) + + self.nodeEditorData.connectNode(outPort: floatGeneratorNode1.outPorts[0], inPort: floatAddNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode2.outPorts[0], inPort: floatAddNode.inPorts[1]) + self.nodeEditorData.forceUpdate() + })) + + alertController.addAction(UIAlertAction(title: "Complete > Advanced Usages", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + let uvNode : Vec2TexCoordNodeData = Vec2TexCoordNodeData() + uvNode.frame = CGRect.init(x: 16, y: 16, width: uvNode.frame.size.width, height: uvNode.frame.size.height) + self.nodeEditorData.addNode(node: uvNode) + + let uvSplitNode : Vec2ChannelSplitNodeData = Vec2ChannelSplitNodeData() + uvSplitNode.frame = CGRect.init(x: 250, y: 16, width: uvSplitNode.frame.size.width, height: uvSplitNode.frame.size.height) + self.nodeEditorData.addNode(node: uvSplitNode) + self.nodeEditorData.connectNode(outPort: uvNode.outPorts[0], inPort: uvSplitNode.inPorts[0]) + + let timeNode : FloatTimeNodeData = FloatTimeNodeData() + timeNode.frame = CGRect.init(x: 16, y: 300, width: timeNode.frame.size.width, height: timeNode.frame.size.height) + self.nodeEditorData.addNode(node: timeNode) + + let sinNode : FloatSinNodeData = FloatSinNodeData() + sinNode.frame = CGRect.init(x: 250, y: 200, width: sinNode.frame.size.width, height: sinNode.frame.size.height) + self.nodeEditorData.addNode(node: sinNode) + self.nodeEditorData.connectNode(outPort: timeNode.outPorts[0], inPort: sinNode.inPorts[0]) + + let vec4CombineNode : Vec4ChannelCombineNodeData = Vec4ChannelCombineNodeData() + vec4CombineNode.frame = CGRect.init(x: 550, y: 70, width: vec4CombineNode.frame.size.width, height: vec4CombineNode.frame.size.height) + self.nodeEditorData.addNode(node: vec4CombineNode) + self.nodeEditorData.connectNode(outPort: uvSplitNode.outPorts[0], inPort: vec4CombineNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: uvSplitNode.outPorts[1], inPort: vec4CombineNode.inPorts[1]) + self.nodeEditorData.connectNode(outPort: sinNode.outPorts[0], inPort: vec4CombineNode.inPorts[2]) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute: + { + self.nodeEditorData.forceUpdate() + }) + })) + + alertController.addAction(UIAlertAction(title: "Complete > VJ Machine", style: .default, handler: { (action) in + self.nodeEditorData.removeAll() + + let atanNode : FloatUVCenterAtanNodeData = FloatUVCenterAtanNodeData() + atanNode.frame = CGRect.init(x: 16, y: 16, width: atanNode.frame.size.width, height: atanNode.frame.size.height) + self.nodeEditorData.addNode(node: atanNode) + + let audioNode : FloatAudioDBNodeData = FloatAudioDBNodeData() + audioNode.frame = CGRect.init(x: 16, y: 300, width: audioNode.frame.size.width, height: audioNode.frame.size.height) + self.nodeEditorData.addNode(node: audioNode) + + let floatGeneratorNode1 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode1.frame = CGRect.init(x: 16, y: 400, width: floatGeneratorNode1.frame.size.width, height: floatGeneratorNode1.frame.size.height) + floatGeneratorNode1.value.value = 5.0 + self.nodeEditorData.addNode(node: floatGeneratorNode1) + + let floatGeneratorNode2 : FloatGeneratorNodeData = FloatGeneratorNodeData() + floatGeneratorNode2.frame = CGRect.init(x: 16, y: 640, width: floatGeneratorNode2.frame.size.width, height: floatGeneratorNode2.frame.size.height) + floatGeneratorNode2.value.value = 8.0 + self.nodeEditorData.addNode(node: floatGeneratorNode2) + + let discRayNode : FloatRayDiscNodeData = FloatRayDiscNodeData() + discRayNode.frame = CGRect.init(x: 300, y: 60, width: discRayNode.frame.size.width, height: discRayNode.frame.size.height) + self.nodeEditorData.addNode(node: discRayNode) + self.nodeEditorData.connectNode(outPort: atanNode.outPorts[0], inPort: discRayNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: audioNode.outPorts[0], inPort: discRayNode.inPorts[1]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode1.outPorts[0], inPort: discRayNode.inPorts[2]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode2.outPorts[0], inPort: discRayNode.inPorts[3]) + + let floatGeneratorNode3 : FloatSliderGeneratorNodeData = FloatSliderGeneratorNodeData() + floatGeneratorNode3.frame = CGRect.init(x: 320, y: 470, width: floatGeneratorNode3.frame.size.width, height: floatGeneratorNode3.frame.size.height) + floatGeneratorNode3.value.value = 0.0 + self.nodeEditorData.addNode(node: floatGeneratorNode3) + + let floatGeneratorNode4 : FloatSliderGeneratorNodeData = FloatSliderGeneratorNodeData() + floatGeneratorNode4.frame = CGRect.init(x: 370, y: 650, width: floatGeneratorNode4.frame.size.width, height: floatGeneratorNode4.frame.size.height) + floatGeneratorNode4.value.value = 0.3 + self.nodeEditorData.addNode(node: floatGeneratorNode4) + + let circleOutlineNode : FloatCircleOutlineNodeData = FloatCircleOutlineNodeData() + circleOutlineNode.frame = CGRect.init(x: 550, y: 70, width: circleOutlineNode.frame.size.width, height: circleOutlineNode.frame.size.height) + self.nodeEditorData.addNode(node: circleOutlineNode) + + self.nodeEditorData.connectNode(outPort: discRayNode.outPorts[0], inPort: circleOutlineNode.inPorts[0]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode3.outPorts[0], inPort: circleOutlineNode.inPorts[1]) + self.nodeEditorData.connectNode(outPort: floatGeneratorNode4.outPorts[0], inPort: circleOutlineNode.inPorts[2]) + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute: + { + self.nodeEditorData.forceUpdate() + }) + })) + + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (action) in + + })) + alertController.popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem + present(alertController, animated: true, completion: {}) + } //MARK: PlaygroundLiveViewMessageHandler public func receive(_ message: PlaygroundValue) { + // guard case let .string(messageString) = message else { return } } public func send(_ message: PlaygroundValue) { diff --git a/README.md b/README.md index b969772..0fdf6e2 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,4 @@ -# Playground Book Xcode Project # +# WWDC 20 PROJECT -## Overview ## - -Welcome to the Playground Book Xcode project! This Xcode project is set up to produce two things: - -- A playground book -- An app for debugging the live view - -In support of this, there are five targets in this Xcode project: - -- **PlaygroundBook**: Produces a playground book as its output -- **BookCore**: Compiles the *BookCore* auxiliary module, an auxiliary module that contains the implementation of the live view and any other author-only functionality -- **BookAPI**: Compiles the *BookAPI* auxiliary module, an auxiliary module that is automatically imported into all user code throughout the book -- **UserModule**: Compiles the *UserModule* user module, a blank user module where users may write code -- **LiveViewTestApp**: Produces an app which uses the `Book_Sources` module to show the live view similarly to how it would be shown in Swift Playgrounds - -This project includes the PlaygroundSupport and PlaygroundBluetooth frameworks from Swift Playgrounds to allow the BookCore, BookAPI, UserModule, and LiveViewTestApp targets to take full advantage of those APIs. The supporting content included with this template, including these frameworks, requires Xcode 11.1 to build. Attempting to use this template with another version of Xcode may result in build errors. - -For more information about the playground book file format, see the *[Swift Playgrounds authoring documentation](https://developer.apple.com/go/?id=swift-playgroundbook-authoring)*. - -## First Steps ## - -To get started with this Xcode project, you need to make a few changes to personalize it for your playground book. - -1. Open `BuildSettings.xcconfig` (in the “Config Files” group in the Project navigator) and make the following modifications: - - - Set `BUNDLE_IDENTIFIER_PREFIX` to a value appropriate for your team - - Set `PLAYGROUND_BOOK_FILE_NAME` to a value appropriate for your playground book - -You may also modify `PLAYGROUND_BOOK_CONTENT_VERSION` to set the `ContentVersion` in the book's `Manifest.plist`. `PLAYGROUND_BOOK_CONTENT_IDENTIFIER` may also be modified to customize to set a specific `ContentIdentifier` in the book's `Manifest.plist`. (It defaults to a value based on `BUNDLE_IDENTIFIER_PREFIX` and `PLAYGROUND_BOOK_FILE_NAME`.) - -2. Open `ManifestPlist.strings` (in the “PrivateResources” group in the “PlaygroundBook” group in the Project navigator) and modify the values of the strings to be appropriate for your playground book. - -3. Open the Project Editor, select the LiveViewTestApp target, and select the appropriate Team in the Signing section. (This step is not required if you are only testing with the iOS Simulator.) - -Once you've finished configuring the project, you can build the PlaygroundBook target, which will produce your playground book as a product. (You can access it by opening the “Products” group in the Project navigator, and then right-clicking and selecting “Show in Finder”. From there, you can use AirDrop or other methods to copy the playground book to an iPad running Swift Playgrounds, or double-click it to import it into Swift Playgrounds on your Mac.) - -## Common Tasks ## - -This Xcode project is structured both in Xcode and on-disk in a very particular way to ensure that the book is assembled correctly. Below are guides for accomplishing some common tasks when creating your playground book. - -### Adding a New Auxiliary or User Module ### - -An auxiliary module is a Swift module which vends an API to the playground book without exposing the sources. A user module is a Swift module where the sources are fully editable by users. (For more information about auxiliary and user modules, see the *[Swift Playgrounds authoring documentation](https://developer.apple.com/go/?id=swift-playgroundbook-authoring)*.) - -To create an auxiliary module or user module, you need to create the right folder structure on-disk. You also need to create a static library target which builds the sources to get editor features such as code completion and live issues. This can be accomplished by performing the following steps: - - 1. In the Project navigator, expand the “PlaygroundBook” group - 2. If you are adding an auxiliary module, expand the “Modules” group. If you are adding a user module, expand the “UserModules” group. - 3. Control-click (or right-click) on the “Modules” or “UserModules” group - 4. Choose “New Group” from the context menu - 5. Name the newly-created group `ModuleName.playgroundmodule`, where “ModuleName” is the name of the module you are creatinng - 6. Control-click (or right-click) on the newly-created group - 7. Choose "New Group" from the context menu - 8. Name the newly-created group `Sources` - 9. Control-click (or right-click) on the newly-created group - 10. Choose “New File…” from the context menu, and create a new Swift file using the assistant - 11. Select the top-level Xcode project from the Project navigator - 12. In the Editor menu, select “Add Target…” - 13. Select the iOS platform at the top of the assistant, choose the “Static Library” template, and click the Next button - 14. In the “Product Name” field, put the name of the module, matching the name of the “ModuleName” you chose in step 5 - 15. Fill in the other fields and click the Finish button - 16. In the Project editor, select the project itself, and then select the Info tab - 17. In the Configurations outline view, expand all build configurations - 18. For each build configuration, in the row for the newly-added target, click “None” and choose “ModuleOverridingBuildSettings” from the pop-up menu - 19. In the Project editor, select the newly-added target, and then select the Build Settings tab - 20. Click “All” in the filter bar to show all build settings - 21. Click on a build setting to select it, and then open the Edit menu and click “Select All” - 22. Press the Delete key on your keyboard to remove all build setting overrides at the target level - 23. Select the Build Phases tab - 24. Expand the “Compile Sources” build phase - 25. Remove the source file that is listed in the “Compile Sources” build phase - 26. From the Project navigator, drag the source file added in step 10 into the “Compile Sources” build phase - 27. In the Project navigator, locate the group named after the target added in steps 12—15 (i.e. “ModuleName” **without** the “.playgroundbook” extension) - 28. Control-click (or right-click) on this group - 29. Choose “Delete” from the context menu - 30. Select “Move to Trash” in the confirmation dialog to delete the source files - -These steps give you a configured, standalone auxiliary or user module. As you’re using the module you’ve just created, it’s important to confirm the following things: - - - The source files for the auxiliary or user module **must** be inside of the “Sources” folder in the “ModuleName.playgroundmodule” folder in the “Modules” or “UserModules” folder on-disk. If not, they will not be copied into the final playground book - - You must specify explicit target dependencies in the Project editor to ensure that the modules build in the correct order - - If you want to use this module from LiveViewTestApp, you must specify an explicit target dependency from LiveViewTestApp to this module - -### Deleting an Auxiliary or User Module ### - -To delete an auxiliary module or user module, you need to delete three things: the target for the library which builds the module, the group in the Xcode project for the module, and the on-disk folder for the module. This can be accomplished by performing the following steps: - - 1. Select the top-level Xcode project from the Project navigator - 2. Select the target for the module you wish to delete - 3. Control-click (or right-click) on the target for the module - 4. Choose “Delete” from the context menu and confirm the deletion - 5. Control-click (or right-click) on the group for the module you wish to delete in the Project navigator (e.g. ModuleName.playgroundmodule) - 6. Choose “Delete” from the context menu - 7. Select “Move to Trash” in the confirmation dialog to delete the source files - 8. Expand the “Supporting Content” group (a child of the “PlaygroundBook” group) in the Project navigator - 9. Find the module you wish to delete in the “Modules” or “UserModules” folder - 10. Control-click (or right-click) on the folder for the module you wish to delete - 11. Choose “Delete” from the context menu and confirm the deletion - -### Renaming an Auxiliary or User Module ### - -To rename an auxiliary or user module, rename both the “ModuleName.playgroundmodule” group and “ModuleName” target to the new name (e.g. “NewModuleName.playgroundmodule” and “NewModuleName”, respectively). If you are renaming an auxiliary module which is in the `UserAutoImportedAuxiliaryModules` array in the book-level Manifest.plist, you will need to rename it there as well; otherwise, no further changes are required. - -### Adding Sources to an Auxiliary or User Module ### - -In order to work correctly throughout this project, source files must be added to the Xcode project, to the module's static library target, and to the `ModuleName.playgroundmodule/Sources` folder on disk. To add a new source file, you can either use the *File > New > File…* menu item, or right-click on the “Sources” group in the “ModuleName.playgroundmodule” group in the Project navigator and select *New File…*. - -In the assistant, select the appropriate template (either “Swift File” or “Cocoa Touch Class”, typically). When the assistant presents a sheet to save the new file, ensure the following is true: - - - The destination of the save sheet is the `Sources` directory inside of the `ModuleName.playgroundmodule` directory (where “ModuleName” is the name of the module to which you are adding a source file) - - The new source file is being added to the ModuleName target (and no others) - -Adding a source file to the previously-mentioned “Sources” group should default to a location where both of those are true. - -If a source file is not in the correct `Sources` directory on-disk, then it will not be copied into the correct location in the final playground book and will not be usable in Swift Playgrounds. - -If a source file is not a member of the module's static library target, then it will not be compiled in Xcode. This means that syntax highlighting, code completion, and other editor features will not work in that source file, and the API it provides will not be usable in other source files in the project. - -**Note**: Only Swift sources are supported in playground books. This Xcode project will not enforce that requirement, but if any non-Swift source files are present in the final playground book, Swift Playgrounds will ignore them. - -### Adding Book-Level PrivateResources ### - -To add content to the book-level `PrivateResources` directory, add the resource file to the Xcode project, and ensure it is a member of the “Copy Bundle Resources” build phase of the PlaygroundBook target. It will be compiled if necessary (as is the case for xibs, storyboards, asset catalogs, and some other resource types) and then copied in to the playground book's `PrivateResources` directory. - -### Adding Book-Level PublicResources ### - -This Xcode project does not support book-level `PublicResources` by default. To add a `PublicResources` directory to your book, do the following: - - 1. Create `PublicResources` directory in Finder in the `PlaygroundBook` directory - 2. Add the `PublicResources` directory to the “PlaygroundBook” group in Xcode as a folder reference (not as a group reference) - 3. Add the `PublicResources` directory to the “Copy Book Contents” build phase in the PlaygroundBook target in the Project editor - -Unlike `PrivateResources`, the contents of the `PublicResources` directory are only copied. They will not be compiled; if you use compiled resources, you must treat them as `PrivateResources`. - -### Adding Chapters, Pages, or Chapter- or Page-Level Content ## - -This Xcode project has limited support for editing the chapters and pages in your playground book. The `Chapters` directory is present in the Xcode project as a folder. You may add `.playgroundchapter` packages there, and they will automatically be copied into the final playground book. - -When adding a chapter or a page, you must also edit the book's or chapter's `Manifest.plist` file to reference the new chapter or page. - -This Xcode project does not support syntax highlighting, code completion, live issues, or other advanced editor features inside of chapters and pages. It is therefore recommended that as much source code as possible be included in the auxiliary modules, and that the chapters and pages have as little source code as possible. - -### Testing the Live View ### - -This Xcode project includes support for testing your playground book's live view. This testing support assumes that the live view for your playground book is implemented in BookCore auxiliary module. If it is implemented elsewhere (i.e. in a page's `Contents.swift` or `LiveView.swift` file), then it cannot easily be tested using this mechanism. - -To test your live view, run the LiveViewTestApp app. This app, which works on iPad, in the iOS Simulator, and as a Mac Catalyst app, is capable of displaying a live view in a manner similar to how Swift Playgrounds would display it. Most notably, LiveViewTestApp will correctly configure the live view safe area layout guides exposed by the PlaygroundSupport framework. - -To configure your live view, implement the `setUpLiveView()` method in `AppDelegate.swift`. This should return a `UIView` or `UIViewController` which is ready to be used as the live view. - -By default, LiveViewTestApp will run your live view in full screen. LiveViewTestApp is also able to run your live view in a side-by-side view (as if it were in Swift Playgrounds with the source code editor either next to or below the live view). To enable this, make the `liveViewConfiguration` property in `AppDelegate.swift` return `.sideBySide` instead of `.fullScreen`. +A continuation of the Shader Node Editor project which failed the last WWDC (19) scholarship. +Now fixed for iOS 13 (Dark Mode, Swift UI, Context Menu, Combine). \ No newline at end of file