From 89d8e6d7a0f02214a73cb3b521cbab322cf1ff76 Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Fri, 27 Jan 2023 13:01:27 +0100 Subject: [PATCH 1/6] added some unit tests for button and basic touches --- .github/workflows/swift-build.yml | 16 +-- Package.swift | 4 +- .../RUILongTouchGestureRecognizer.swift | 2 +- Tests/RealityUITests/RUIButtonTests.swift | 83 ++++++++++++ .../RUILongTouchButtonTests.swift | 125 ++++++++++++++++++ 5 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 Tests/RealityUITests/RUIButtonTests.swift create mode 100644 Tests/RealityUITests/RUILongTouchButtonTests.swift diff --git a/.github/workflows/swift-build.yml b/.github/workflows/swift-build.yml index e25bdb6..f8e03f5 100644 --- a/.github/workflows/swift-build.yml +++ b/.github/workflows/swift-build.yml @@ -12,6 +12,8 @@ on: jobs: build: + env: + SCHEME: RealityUI runs-on: macOS-12 steps: - uses: actions/checkout@v3 @@ -22,15 +24,9 @@ jobs: swift package generate-xcodeproj - name: Test iOS run: | - xcodebuild clean build -project $PROJECT -scheme $SCHEME -sdk $SDK CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO | xcpretty - env: - PROJECT: RealityUI.xcodeproj - SCHEME: RealityUI-Package - SDK: iphoneos + xcodebuild build -scheme $SCHEME -destination "generic/platform=iOS" | xcpretty + xcodebuild test -scheme $SCHEME -destination "platform=iOS Simulator,name=iPhone 14" | xcpretty - name: Test macOS run: | - xcodebuild clean build -project $PROJECT -scheme $SCHEME -sdk $SDK CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO | xcpretty - env: - PROJECT: RealityUI.xcodeproj - SCHEME: RealityUI-Package - SDK: macosx + xcodexcodebuild build -scheme $SCHEME -destination "platform=macOS" | xcpretty + xcodebuild test -scheme $SCHEME -destination "platform=macOS" | xcpretty diff --git a/Package.swift b/Package.swift index b022412..8c1ffbe 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( ], dependencies: [], targets: [ - .target(name: "RealityUI", dependencies: []) -// .testTarget(name: "RealityUITests", dependencies: ["RealityUI"]) + .target(name: "RealityUI", dependencies: []), + .testTarget(name: "RealityUITests", dependencies: ["RealityUI"]) ] ) diff --git a/Sources/RealityUI/RUILongTouchGestureRecognizer.swift b/Sources/RealityUI/RUILongTouchGestureRecognizer.swift index a743a3b..3d7f257 100644 --- a/Sources/RealityUI/RUILongTouchGestureRecognizer.swift +++ b/Sources/RealityUI/RUILongTouchGestureRecognizer.swift @@ -122,7 +122,7 @@ public protocol HasTouchUpInside: HasARTouch {} self.viewSubscriber = self.arView.scene.subscribe(to: SceneEvents.Update.self, updateRUILongTouch(_:)) } - func updateRUILongTouch(_ event: SceneEvents.Update) { + func updateRUILongTouch(_ event: SceneEvents.Update?) { guard let touchLocation = self.touchLocation, let hitEntity = self.entity else { diff --git a/Tests/RealityUITests/RUIButtonTests.swift b/Tests/RealityUITests/RUIButtonTests.swift new file mode 100644 index 0000000..e3b9c0a --- /dev/null +++ b/Tests/RealityUITests/RUIButtonTests.swift @@ -0,0 +1,83 @@ +// +// RUIButtonTests.swift +// +// +// Created by Max Cobb on 25/01/2023. +// + +import XCTest +@testable import RealityUI + +final class RUIButtonTests: XCTestCase { + + var button: RUIButton! + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + button = RUIButton() + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testDefaultInitialization() { + XCTAssertNotNil(button) + } + + func testTouchUpCompletedCallback() { + let expectation = self.expectation(description: "touchUpCompleted callback was called") + button.touchUpCompleted = { _ in + expectation.fulfill() + } + button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchEnded(nil) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testTouchUpCompletedNotCalled() { + let expectation = self.expectation(description: "touchUpCompleted callback was not called") + expectation.isInverted = true + button.touchUpCompleted = { _ in + expectation.fulfill() + } + button.arTouchEnded(nil) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testTouchUpCompletedNotCalledTouchMoved() { + let expectation = self.expectation(description: "touchUpCompleted callback was not called") + expectation.isInverted = true + button.touchUpCompleted = { _ in + expectation.fulfill() + } + button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchUpdated(SIMD3(5,5,0), hasCollided: false) + button.arTouchEnded(nil) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testCompressButton() { + button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + XCTAssertTrue(button.button.isCompressed) + button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + } + + func testCancelReleaseButton() { + let expectation = self.expectation(description: "touchUpCompleted callback was not called") + expectation.isInverted = true + button.touchUpCompleted = { _ in + expectation.fulfill() + } + button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchCancelled() + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testChangeColour() { + button.baseColor = .black + button.buttonColor = .orange + XCTAssertEqual(button.baseColor, .black) + XCTAssertEqual(button.buttonColor, .orange) + } +} diff --git a/Tests/RealityUITests/RUILongTouchButtonTests.swift b/Tests/RealityUITests/RUILongTouchButtonTests.swift new file mode 100644 index 0000000..32b5925 --- /dev/null +++ b/Tests/RealityUITests/RUILongTouchButtonTests.swift @@ -0,0 +1,125 @@ +// +// RUILongTouchButtonTests.swift +// +// +// Created by Max Cobb on 25/01/2023. +// + +import XCTest +import RealityKit +#if canImport(UIKit) +import UIKit.UITouch +#endif +@testable import RealityUI + +final class RUILongTouchButtonTests: XCTestCase { + + var gestureRecognizer: RUILongTouchGestureRecognizer! + var arView: ARView! + var entity: RUIButton! + + override func setUpWithError() throws { + arView = ARView(frame: .init(origin: .zero, size: CGSize(width: 256, height: 256))) + gestureRecognizer = RUILongTouchGestureRecognizer(target: nil, action: nil, view: arView) + RealityUI.enableGestures(.longTouch, on: arView) + entity = RUIButton() + let anchor = AnchorEntity() + let cam = PerspectiveCamera() + cam.look(at: .zero, from: [0, 1, 1], relativeTo: nil) + anchor.addChild(cam) + anchor.addChild(entity) + arView.scene.addAnchor(anchor) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + arView.removeFromSuperview() + arView.scene.anchors.forEach { $0.removeFromParent() } + #if os(iOS) + arView.gestureRecognizers?.removeAll() + arView.session.pause() + #elseif os(macOS) + arView.gestureRecognizers.removeAll() + #endif + arView = nil + } + + #if os(iOS) + func testGlobalTouchBegan() { + let mytouch = TestTouch(location: CGPoint(x: 128, y: 128)) + gestureRecognizer.touchesBegan([mytouch], with: UIEvent()) + XCTAssertEqual(gestureRecognizer.entity, entity) + XCTAssertTrue(entity.isCompressed) + gestureRecognizer.touchesEnded([mytouch], with: UIEvent()) + XCTAssertFalse(entity.isCompressed) + } + + func testTouchMissed() { + let mytouch = TestTouch(location: .zero) + gestureRecognizer.touchesBegan([mytouch], with: UIEvent()) + XCTAssertNil(gestureRecognizer.entity) + XCTAssertFalse(entity.isCompressed) + gestureRecognizer.touchesEnded([mytouch], with: UIEvent()) + } + + func testTouchMovedOutAndBack() { + let mytouch = TestTouch(location: CGPoint(x: 128, y: 128)) + gestureRecognizer.touchesBegan([mytouch], with: UIEvent()) + XCTAssertEqual(gestureRecognizer.entity, entity) + XCTAssertTrue(entity.isCompressed) + XCTAssertNotNil(gestureRecognizer.viewSubscriber, "Subscriber has not been created") + + mytouch.updateLocation(to: .zero) + gestureRecognizer.touchesMoved([mytouch], with: UIEvent()) + gestureRecognizer.updateRUILongTouch(nil) + XCTAssertFalse(entity.isCompressed) + + mytouch.updateLocation(to: CGPoint(x: 128, y: 128)) + gestureRecognizer.touchesMoved([mytouch], with: UIEvent()) + gestureRecognizer.updateRUILongTouch(nil) + XCTAssertTrue(entity.isCompressed) + + gestureRecognizer.touchesEnded([mytouch], with: UIEvent()) + } + #elseif os(macOS) + func testGlobalTouchBegan() { + var event: NSEvent! = NSEvent.mouseEvent( + with: .leftMouseDown, location: CGPoint(x: 128, y: 128), + modifierFlags: [], timestamp: 0, windowNumber: 0, + context: nil, eventNumber: 0, clickCount: 1, pressure: 1) + gestureRecognizer.mouseDown(with: event) + XCTAssertEqual(gestureRecognizer.entity, entity) + XCTAssertTrue(entity.isCompressed) + + let expectation = self.expectation(description: "touchUpCompleted callback was called") + entity.touchUpCompleted = { _ in + expectation.fulfill() + } + + event = NSEvent.mouseEvent( + with: .leftMouseUp, location: CGPoint(x: 128, y: 128), + modifierFlags: [], timestamp: 0, windowNumber: 0, + context: nil, eventNumber: 0, clickCount: 1, pressure: 1) + gestureRecognizer.mouseUp(with: event) + waitForExpectations(timeout: 0.5, handler: nil) + } + #endif +} + +#if os(iOS) +fileprivate class TestTouch: UITouch { + var currentLocation: CGPoint + + init(location: CGPoint) { + self.currentLocation = location + } + + override func location(in view: UIView?) -> CGPoint { + return self.currentLocation + } + + func updateLocation(to location: CGPoint) { + self.currentLocation = location + } +} +#endif From 78e58c1c388441cc2c6c60f671a9d46b1c7ad93e Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Fri, 27 Jan 2023 13:30:33 +0100 Subject: [PATCH 2/6] updated for swiftlint --- Tests/RealityUITests/RUIButtonTests.swift | 12 ++++++------ Tests/RealityUITests/RUILongTouchButtonTests.swift | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/RealityUITests/RUIButtonTests.swift b/Tests/RealityUITests/RUIButtonTests.swift index e3b9c0a..6d35d94 100644 --- a/Tests/RealityUITests/RUIButtonTests.swift +++ b/Tests/RealityUITests/RUIButtonTests.swift @@ -30,7 +30,7 @@ final class RUIButtonTests: XCTestCase { button.touchUpCompleted = { _ in expectation.fulfill() } - button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchStarted(SIMD3(0, 0, 0), hasCollided: true) button.arTouchEnded(nil) waitForExpectations(timeout: 0.1, handler: nil) } @@ -51,16 +51,16 @@ final class RUIButtonTests: XCTestCase { button.touchUpCompleted = { _ in expectation.fulfill() } - button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) - button.arTouchUpdated(SIMD3(5,5,0), hasCollided: false) + button.arTouchStarted(SIMD3(0, 0, 0), hasCollided: true) + button.arTouchUpdated(SIMD3(5, 5, 0), hasCollided: false) button.arTouchEnded(nil) waitForExpectations(timeout: 0.1, handler: nil) } func testCompressButton() { - button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchStarted(SIMD3(0, 0, 0), hasCollided: true) XCTAssertTrue(button.button.isCompressed) - button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchStarted(SIMD3(0, 0, 0), hasCollided: true) } func testCancelReleaseButton() { @@ -69,7 +69,7 @@ final class RUIButtonTests: XCTestCase { button.touchUpCompleted = { _ in expectation.fulfill() } - button.arTouchStarted(SIMD3(0,0,0), hasCollided: true) + button.arTouchStarted(SIMD3(0, 0, 0), hasCollided: true) button.arTouchCancelled() waitForExpectations(timeout: 0.1, handler: nil) } diff --git a/Tests/RealityUITests/RUILongTouchButtonTests.swift b/Tests/RealityUITests/RUILongTouchButtonTests.swift index 32b5925..e22bdb6 100644 --- a/Tests/RealityUITests/RUILongTouchButtonTests.swift +++ b/Tests/RealityUITests/RUILongTouchButtonTests.swift @@ -107,7 +107,7 @@ final class RUILongTouchButtonTests: XCTestCase { } #if os(iOS) -fileprivate class TestTouch: UITouch { +private class TestTouch: UITouch { var currentLocation: CGPoint init(location: CGPoint) { From ce7c1a9ea8f28ca985aff40bf003bdb63db07185 Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Sun, 29 Jan 2023 12:29:31 +0100 Subject: [PATCH 3/6] added RUISlider tests --- .../RUILongTouchButtonTests.swift | 2 +- .../RUILongTouchSliderTests.swift | 63 +++++++++++++++ Tests/RealityUITests/RUISliderTests.swift | 78 +++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Tests/RealityUITests/RUILongTouchSliderTests.swift create mode 100644 Tests/RealityUITests/RUISliderTests.swift diff --git a/Tests/RealityUITests/RUILongTouchButtonTests.swift b/Tests/RealityUITests/RUILongTouchButtonTests.swift index e22bdb6..f219b22 100644 --- a/Tests/RealityUITests/RUILongTouchButtonTests.swift +++ b/Tests/RealityUITests/RUILongTouchButtonTests.swift @@ -107,7 +107,7 @@ final class RUILongTouchButtonTests: XCTestCase { } #if os(iOS) -private class TestTouch: UITouch { +internal class TestTouch: UITouch { var currentLocation: CGPoint init(location: CGPoint) { diff --git a/Tests/RealityUITests/RUILongTouchSliderTests.swift b/Tests/RealityUITests/RUILongTouchSliderTests.swift new file mode 100644 index 0000000..cf3e859 --- /dev/null +++ b/Tests/RealityUITests/RUILongTouchSliderTests.swift @@ -0,0 +1,63 @@ +// +// RUILongTouchSliderTests.swift +// +// +// Created by Max Cobb on 29/01/2023. +// + +import XCTest +import RealityKit +@testable import RealityUI + +final class RUILongTouchSliderTests: XCTestCase { + + var gestureRecognizer: RUILongTouchGestureRecognizer! + var arView: ARView! + var entity: RUISlider! + + override func setUpWithError() throws { + arView = ARView(frame: .init(origin: .zero, size: CGSize(width: 256, height: 256))) + gestureRecognizer = RUILongTouchGestureRecognizer(target: nil, action: nil, view: arView) + RealityUI.enableGestures(.longTouch, on: arView) + entity = RUISlider(length: 10, start: 0.5) + let anchor = AnchorEntity() + let cam = PerspectiveCamera() + cam.look(at: .zero, from: [0, 0, -10], relativeTo: nil) + anchor.addChild(cam) + anchor.addChild(entity) + arView.scene.addAnchor(anchor) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + arView.removeFromSuperview() + arView.scene.anchors.forEach { $0.removeFromParent() } + #if os(iOS) + arView.gestureRecognizers?.removeAll() + arView.session.pause() + #elseif os(macOS) + arView.gestureRecognizers.removeAll() + #endif + arView = nil + } + + #if os(iOS) + func testSlideUpDownMiddle() { + let mytouch = TestTouch(location: CGPoint(x: 128, y: 128)) + gestureRecognizer.touchesBegan([mytouch], with: UIEvent()) + XCTAssertEqual(gestureRecognizer.entity, entity) + mytouch.updateLocation(to: CGPoint(x: 255, y: 128)) + gestureRecognizer.touchesMoved([mytouch], with: UIEvent()) + gestureRecognizer.updateRUILongTouch(nil) + XCTAssertEqual(entity.value, 1) + mytouch.updateLocation(to: CGPoint(x: 1, y: 200)) + gestureRecognizer.touchesMoved([mytouch], with: UIEvent()) + gestureRecognizer.updateRUILongTouch(nil) + XCTAssertEqual(entity.value, 0) + mytouch.updateLocation(to: CGPoint(x: 128, y: 50)) + gestureRecognizer.touchesMoved([mytouch], with: UIEvent()) + gestureRecognizer.updateRUILongTouch(nil) + XCTAssertEqual(entity.value, 0.5) + } + #endif +} diff --git a/Tests/RealityUITests/RUISliderTests.swift b/Tests/RealityUITests/RUISliderTests.swift new file mode 100644 index 0000000..cef7b9d --- /dev/null +++ b/Tests/RealityUITests/RUISliderTests.swift @@ -0,0 +1,78 @@ +// +// RUISliderTests.swift +// +// +// Created by Max Cobb on 29/01/2023. +// + +import XCTest +import RealityKit +@testable import RealityUI + +final class RUISliderTests: XCTestCase { + + override func setUpWithError() throws { + let slider = RUISlider() + XCTAssertNotNil(slider) + XCTAssertEqual(slider.slider.length, 10) + XCTAssertEqual(slider.slider.value, 0) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testInitWithLengthStepsAndStart() { + let slider = RUISlider(length: 20, start: 0.5, steps: 2) + XCTAssertEqual(slider.slider.length, 20) + XCTAssertEqual(slider.slider.value, 0.5) + XCTAssertEqual(slider.slider.steps, 2) + } + + func testARTouchMovedHalfway() { + let slider = RUISlider() + let worldCoordinate = SIMD3.zero + var value = slider.value + slider.arTouchStarted(worldCoordinate) + XCTAssertEqual(value, slider.value) + slider.arTouchUpdated([-5, 0, 0]) + XCTAssertEqual(slider.value, 0.5, accuracy: 0.0001) + slider.arTouchUpdated([-5, 0, 0]) + XCTAssertEqual(slider.value, 0.5, accuracy: 0.0001) + slider.arTouchEnded() + XCTAssertEqual(slider.value, 0.5, accuracy: 0.0001) + } + + func testARTouchCancelled() { + let slider = RUISlider() + let worldCoordinate = SIMD3.zero + var value = slider.value + slider.arTouchStarted(worldCoordinate) + XCTAssertEqual(value, slider.value) + slider.arTouchCancelled() + XCTAssertEqual(value, slider.value) + } + + func testAnimateSliderThumbPos() { + let arView = ARView(frame: CoreFoundation.CGRect(origin: .zero, size: CGSize(width: 256, height: 256))) + let anchor = AnchorEntity(world: .zero) + let slider = RUISlider(length: 10, start: 0.5) + anchor.addChild(slider) + arView.scene.addAnchor(anchor) + XCTAssertEqual(slider.value, 0.5) + slider.setPercent(to: 0.9, animated: true) + var expectation = self.expectation(description: "wait for it") + expectation.isInverted = true + waitForExpectations(timeout: 0.3, handler: nil) + var xpos = slider.getModel(part: "thumb")!.position.x + XCTAssertEqual(xpos, -4, accuracy: 0.0005) + + slider.setPercent(to: 0.2, animated: true) + expectation = self.expectation(description: "wait for it") + expectation.isInverted = true + waitForExpectations(timeout: 0.3, handler: nil) + xpos = slider.getModel(part: "thumb")!.position.x + XCTAssertEqual(xpos, 3, accuracy: 0.0005) + } + +} From 483f8e8dfaf3773cd2901e6e1121bbe45e187333 Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Mon, 30 Jan 2023 12:35:41 +0100 Subject: [PATCH 4/6] added RUIStepper and RUISlider tests. coverage up to 66% --- .../RUILongTouchSliderTests.swift | 18 ++++- Tests/RealityUITests/RUISliderTests.swift | 10 +-- Tests/RealityUITests/RUIStepperTests.swift | 43 +++++++++++ Tests/RealityUITests/RUISwitchTests.swift | 72 +++++++++++++++++++ 4 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 Tests/RealityUITests/RUIStepperTests.swift create mode 100644 Tests/RealityUITests/RUISwitchTests.swift diff --git a/Tests/RealityUITests/RUILongTouchSliderTests.swift b/Tests/RealityUITests/RUILongTouchSliderTests.swift index cf3e859..0a20798 100644 --- a/Tests/RealityUITests/RUILongTouchSliderTests.swift +++ b/Tests/RealityUITests/RUILongTouchSliderTests.swift @@ -59,5 +59,21 @@ final class RUILongTouchSliderTests: XCTestCase { gestureRecognizer.updateRUILongTouch(nil) XCTAssertEqual(entity.value, 0.5) } - #endif + + func testDoubleTouchesBegan() { + let mytouch = TestTouch(location: CGPoint(x: 128, y: 128)) + gestureRecognizer.touchesBegan([mytouch], with: UIEvent()) + gestureRecognizer.touchesBegan([mytouch], with: UIEvent()) + XCTAssertNil(gestureRecognizer.activeTouch) + } + + func testTwoFingersTouching() { + let mytouches: Set = [ + TestTouch(location: CGPoint(x: 128, y: 128)), + TestTouch(location: CGPoint(x: 200, y: 128)) + ] + gestureRecognizer.touchesBegan(mytouches, with: UIEvent()) + XCTAssertNil(gestureRecognizer.activeTouch) + } +#endif } diff --git a/Tests/RealityUITests/RUISliderTests.swift b/Tests/RealityUITests/RUISliderTests.swift index cef7b9d..073a210 100644 --- a/Tests/RealityUITests/RUISliderTests.swift +++ b/Tests/RealityUITests/RUISliderTests.swift @@ -32,9 +32,9 @@ final class RUISliderTests: XCTestCase { func testARTouchMovedHalfway() { let slider = RUISlider() let worldCoordinate = SIMD3.zero - var value = slider.value + let startValue = slider.value slider.arTouchStarted(worldCoordinate) - XCTAssertEqual(value, slider.value) + XCTAssertEqual(startValue, slider.value) slider.arTouchUpdated([-5, 0, 0]) XCTAssertEqual(slider.value, 0.5, accuracy: 0.0001) slider.arTouchUpdated([-5, 0, 0]) @@ -46,11 +46,11 @@ final class RUISliderTests: XCTestCase { func testARTouchCancelled() { let slider = RUISlider() let worldCoordinate = SIMD3.zero - var value = slider.value + let startValue = slider.value slider.arTouchStarted(worldCoordinate) - XCTAssertEqual(value, slider.value) + XCTAssertEqual(startValue, slider.value) slider.arTouchCancelled() - XCTAssertEqual(value, slider.value) + XCTAssertEqual(startValue, slider.value) } func testAnimateSliderThumbPos() { diff --git a/Tests/RealityUITests/RUIStepperTests.swift b/Tests/RealityUITests/RUIStepperTests.swift new file mode 100644 index 0000000..c7beb4c --- /dev/null +++ b/Tests/RealityUITests/RUIStepperTests.swift @@ -0,0 +1,43 @@ +// +// RUIStepperTests.swift +// +// +// Created by Max Cobb on 29/01/2023. +// + +import XCTest +@testable import RealityUI + +final class RUIStepperTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testStyleInitialization() { + let stepper = RUIStepper(style: StepperComponent.Style.arrowLeftRight) + XCTAssertEqual(stepper.stepper.style, StepperComponent.Style.arrowLeftRight) + } + + func testUpDownTriggers() { + let stepper = RUIStepper() + var upTriggered = false + var downTriggered = false + stepper.upTrigger = { _ in + upTriggered = true + } + stepper.downTrigger = { _ in + downTriggered = true + } + stepper.stepperTap(clicker: stepper, worldTapPos: [0.5, 0, 0]) + XCTAssertTrue(downTriggered) + XCTAssertFalse(upTriggered) + stepper.stepperTap(clicker: stepper, worldTapPos: [-0.5, 0, 0]) + XCTAssertTrue(upTriggered) + } + +} diff --git a/Tests/RealityUITests/RUISwitchTests.swift b/Tests/RealityUITests/RUISwitchTests.swift new file mode 100644 index 0000000..e7a8836 --- /dev/null +++ b/Tests/RealityUITests/RUISwitchTests.swift @@ -0,0 +1,72 @@ +// +// RUISwitchTests.swift +// +// +// Created by Max Cobb on 29/01/2023. +// + +import XCTest +@testable import RealityUI +import RealityKit + +final class RUISwitchTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testSwitchChangedCallback() { + let testSwitch = RUISwitch() + var switchChangedCalled = false + testSwitch.switchChanged = { newVal in + // newval should be true + switchChangedCalled = newVal.isOn + } + testSwitch.setOn(true) + XCTAssertTrue(switchChangedCalled) + } + + func testSwitchOnOffColors() { + let testSwitch = RUISwitch(switchness: SwitchComponent(onColor: .white, offColor: .black)) + let arView = ARView(frame: CoreFoundation.CGRect(origin: .zero, size: CGSize(width: 256, height: 256))) + let anchor = AnchorEntity(world: .zero) + anchor.addChild(testSwitch) + arView.scene.addAnchor(anchor) + XCTAssertEqual(testSwitch.switchness.onColor, Material.Color.white) + XCTAssertEqual(testSwitch.switchness.offColor, Material.Color.black) + XCTAssertFalse(testSwitch.isOn) + XCTAssertGreaterThan(testSwitch.getModel(part: "thumb")!.position.x, 0) + XCTAssertTrue(testSwitch.getModel(part: "thumb")!.position.x > 0) + + guard let bgMat = testSwitch.getModel(part: "background")?.model!.materials[0] as? UnlitMaterial else { + return XCTFail("Cannot get background material") + } + testSwitch.setOn(true) + XCTAssertTrue(testSwitch.isOn) + let expectation = self.expectation(description: "touchUpCompleted callback was not called") + expectation.isInverted = true + waitForExpectations(timeout: 0.3, handler: nil) + XCTAssertLessThan(testSwitch.getModel(part: "thumb")!.position.x, 0) + guard let onMat = testSwitch.getModel(part: "background")?.model!.materials[0] as? UnlitMaterial else { + return XCTFail("Cannot get background material") + } + if #available(iOS 15.0, *) { + XCTAssertNotEqual(bgMat.color.tint, onMat.color.tint) + } + testSwitch.setOn(false) + XCTAssertGreaterThan(testSwitch.getModel(part: "thumb")!.position.x, 0) + XCTAssertFalse(testSwitch.isOn) + guard let offMat = testSwitch.getModel(part: "background")?.model!.materials[0] as? UnlitMaterial else { + return XCTFail("Cannot get background material") + } + if #available(iOS 15.0, *) { + XCTAssertEqual(bgMat.color.tint, offMat.color.tint) + } + print("break") + } + +} From 41e5e1a7d36e4601317ae6f7c9d4ba8ee0d6c1dc Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Mon, 30 Jan 2023 15:13:35 +0100 Subject: [PATCH 5/6] add RUIText tests --- Sources/RealityUI/RUIText.swift | 7 ++-- Tests/RealityUITests/RUITextTests.swift | 54 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 Tests/RealityUITests/RUITextTests.swift diff --git a/Sources/RealityUI/RUIText.swift b/Sources/RealityUI/RUIText.swift index 6483ed6..259b239 100644 --- a/Sources/RealityUI/RUIText.swift +++ b/Sources/RealityUI/RUIText.swift @@ -140,6 +140,7 @@ public extension HasText { var text: String? { get { self.textComponent.text } set { + self.textComponent.text = newValue self.setText(newValue) } } @@ -183,7 +184,7 @@ public extension HasText { /// Change the text currently presented on the HasText Entity /// - Parameter text: New text to be rendered. - func setText(_ text: String?) { + internal func setText(_ text: String?) { guard let text = text else { self.getModel(part: .textEntity)?.model = nil return @@ -226,9 +227,7 @@ public extension HasText { } let visbounds = self.visualBounds(relativeTo: nil) selfCol.collision = CollisionComponent( - shapes: [ShapeResource.generateBox(size: visbounds.extents) - .offsetBy(translation: visbounds.center) - ] + shapes: [ShapeResource.generateBox(size: visbounds.extents).offsetBy(translation: visbounds.center)] ) } } diff --git a/Tests/RealityUITests/RUITextTests.swift b/Tests/RealityUITests/RUITextTests.swift new file mode 100644 index 0000000..3ed095e --- /dev/null +++ b/Tests/RealityUITests/RUITextTests.swift @@ -0,0 +1,54 @@ +// +// RUITextTests.swift +// +// +// Created by Max Cobb on 30/01/2023. +// + +import XCTest +@testable import RealityUI +import RealityKit + +final class RUITextTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testInit() { + let text = RUIText(with: "test text") + XCTAssertNotNil(text) + XCTAssertEqual(text.text, "test text") + } + + func testEmptyInit() { + let text = RUIText() + XCTAssertNotNil(text) + XCTAssertNil(text.text) + } + + func testInitWithComponent() { + let textComponent = TextComponent(text: "test text") + let text = RUIText(textComponent: textComponent) + XCTAssertNotNil(text) + XCTAssertEqual(text.text, "test text") + } + + func testChangeText() { + let textComponent = TextComponent(text: "test text") + let text = RUIText(textComponent: textComponent) + XCTAssertNotNil(text) + XCTAssertEqual(text.text, "test text") + let visualBounds = text.visualBounds(relativeTo: nil) + text.text = "new text, new text" + XCTAssertEqual(text.textComponent.text, "new text, new text") + XCTAssertGreaterThan(text.visualBounds(relativeTo: nil).extents.x, visualBounds.extents.x) + text.text = nil + XCTAssertEqual(text.visualBounds(relativeTo: nil).boundingRadius, 0) + } + +} From a04eac993dddfcbc67ebd06cb9a93532bcaf5143 Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Sat, 4 Feb 2023 09:40:24 +0000 Subject: [PATCH 6/6] added animation tests, some others, and fixed example project --- .github/workflows/swift-build.yml | 13 +-- README.md | 3 + .../project.pbxproj | 88 +++++++------------ .../Entity+Extensions.swift | 18 +--- .../ViewController+NonRealityUI.swift | 2 +- .../ViewController+RealityControls.swift | 9 +- .../RealityUI+Examples/ViewController.swift | 2 +- Sources/RealityUI/RUIAnimations.swift | 49 +++++------ Sources/RealityUI/RUISlider.swift | 2 + Sources/RealityUI/RealityUI.swift | 5 +- Tests/RealityUITests/RUIAnimationTests.swift | 84 ++++++++++++++++++ Tests/RealityUITests/RUIButtonTests.swift | 4 +- Tests/RealityUITests/RUISliderTests.swift | 8 +- Tests/RealityUITests/RUISwitchTests.swift | 22 ++++- .../RealityUIGeneralTests.swift | 52 +++++++++++ 15 files changed, 234 insertions(+), 127 deletions(-) create mode 100644 Tests/RealityUITests/RUIAnimationTests.swift create mode 100644 Tests/RealityUITests/RealityUIGeneralTests.swift diff --git a/.github/workflows/swift-build.yml b/.github/workflows/swift-build.yml index f8e03f5..7515821 100644 --- a/.github/workflows/swift-build.yml +++ b/.github/workflows/swift-build.yml @@ -12,21 +12,16 @@ on: jobs: build: - env: - SCHEME: RealityUI runs-on: macOS-12 steps: - uses: actions/checkout@v3 - name: Swift Lint run: swiftlint --strict - - name: Build Package - run: | - swift package generate-xcodeproj - name: Test iOS run: | - xcodebuild build -scheme $SCHEME -destination "generic/platform=iOS" | xcpretty - xcodebuild test -scheme $SCHEME -destination "platform=iOS Simulator,name=iPhone 14" | xcpretty + xcodebuild build -scheme RealityUI -destination "platform=iOS Simulator,name=iPhone 14" | xcpretty - name: Test macOS run: | - xcodexcodebuild build -scheme $SCHEME -destination "platform=macOS" | xcpretty - xcodebuild test -scheme $SCHEME -destination "platform=macOS" | xcpretty + xcodebuild build -scheme RealityUI -destination "platform=macOS" | xcpretty + env: + SCHEME: RealityUI diff --git a/README.md b/README.md index af967e1..3edc4a7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ The User Interface controls in this repository so far are made to be familiar to + diff --git a/RealityUI+Examples/RealityUI+Examples.xcodeproj/project.pbxproj b/RealityUI+Examples/RealityUI+Examples.xcodeproj/project.pbxproj index 4d81b3e..e6c71cf 100644 --- a/RealityUI+Examples/RealityUI+Examples.xcodeproj/project.pbxproj +++ b/RealityUI+Examples/RealityUI+Examples.xcodeproj/project.pbxproj @@ -8,45 +8,25 @@ /* Begin PBXBuildFile section */ F301D8A8247A62ED004AE1FA /* ViewController+RealityControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = F301D8A7247A62ED004AE1FA /* ViewController+RealityControls.swift */; }; - F33505D525936A7700B8AE86 /* RUILongTouchGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505CA25936A7700B8AE86 /* RUILongTouchGestureRecognizer.swift */; }; - F33505D625936A7700B8AE86 /* HasRUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505CB25936A7700B8AE86 /* HasRUI.swift */; }; - F33505D725936A7700B8AE86 /* RUISwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505CC25936A7700B8AE86 /* RUISwitch.swift */; }; - F33505D825936A7700B8AE86 /* HasClick.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505CD25936A7700B8AE86 /* HasClick.swift */; }; - F33505D925936A7700B8AE86 /* RUIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505CE25936A7700B8AE86 /* RUIButton.swift */; }; - F33505DA25936A7700B8AE86 /* RealityUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505CF25936A7700B8AE86 /* RealityUI.swift */; }; - F33505DB25936A7700B8AE86 /* RUIStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505D025936A7700B8AE86 /* RUIStepper.swift */; }; - F33505DC25936A7700B8AE86 /* RUIAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505D125936A7700B8AE86 /* RUIAnimations.swift */; }; - F33505DD25936A7700B8AE86 /* RUISlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505D225936A7700B8AE86 /* RUISlider.swift */; }; - F33505DE25936A7700B8AE86 /* RUIText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505D325936A7700B8AE86 /* RUIText.swift */; }; - F33505DF25936A7700B8AE86 /* HasTurnTouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33505D425936A7700B8AE86 /* HasTurnTouch.swift */; }; F3414191246FF53E006B1ECA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3414190246FF53E006B1ECA /* AppDelegate.swift */; }; F3414193246FF53E006B1ECA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3414192246FF53E006B1ECA /* ViewController.swift */; }; F3414195246FF540006B1ECA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3414194246FF540006B1ECA /* Assets.xcassets */; }; F3414198246FF540006B1ECA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F3414196246FF540006B1ECA /* LaunchScreen.storyboard */; }; F34141B12471A595006B1ECA /* ShowTime in Frameworks */ = {isa = PBXBuildFile; productRef = F34141B02471A595006B1ECA /* ShowTime */; }; + F361A60F298D58A8006606BC /* RealityUI in Frameworks */ = {isa = PBXBuildFile; productRef = F361A60E298D58A8006606BC /* RealityUI */; }; F38598C2247AE28F007BBC88 /* Entity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38598C1247AE28F007BBC88 /* Entity+Extensions.swift */; }; F3DB9D81247D8BF8006D6CE5 /* ViewController+NonRealityUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DB9D80247D8BF8006D6CE5 /* ViewController+NonRealityUI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ F301D8A7247A62ED004AE1FA /* ViewController+RealityControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+RealityControls.swift"; sourceTree = ""; }; - F33505CA25936A7700B8AE86 /* RUILongTouchGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUILongTouchGestureRecognizer.swift; path = ../Sources/RealityUI/RUILongTouchGestureRecognizer.swift; sourceTree = SOURCE_ROOT; }; - F33505CB25936A7700B8AE86 /* HasRUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HasRUI.swift; path = ../Sources/RealityUI/HasRUI.swift; sourceTree = SOURCE_ROOT; }; - F33505CC25936A7700B8AE86 /* RUISwitch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUISwitch.swift; path = ../Sources/RealityUI/RUISwitch.swift; sourceTree = SOURCE_ROOT; }; - F33505CD25936A7700B8AE86 /* HasClick.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HasClick.swift; path = ../Sources/RealityUI/HasClick.swift; sourceTree = SOURCE_ROOT; }; - F33505CE25936A7700B8AE86 /* RUIButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUIButton.swift; path = ../Sources/RealityUI/RUIButton.swift; sourceTree = SOURCE_ROOT; }; - F33505CF25936A7700B8AE86 /* RealityUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RealityUI.swift; path = ../Sources/RealityUI/RealityUI.swift; sourceTree = SOURCE_ROOT; }; - F33505D025936A7700B8AE86 /* RUIStepper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUIStepper.swift; path = ../Sources/RealityUI/RUIStepper.swift; sourceTree = SOURCE_ROOT; }; - F33505D125936A7700B8AE86 /* RUIAnimations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUIAnimations.swift; path = ../Sources/RealityUI/RUIAnimations.swift; sourceTree = SOURCE_ROOT; }; - F33505D225936A7700B8AE86 /* RUISlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUISlider.swift; path = ../Sources/RealityUI/RUISlider.swift; sourceTree = SOURCE_ROOT; }; - F33505D325936A7700B8AE86 /* RUIText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RUIText.swift; path = ../Sources/RealityUI/RUIText.swift; sourceTree = SOURCE_ROOT; }; - F33505D425936A7700B8AE86 /* HasTurnTouch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HasTurnTouch.swift; path = ../Sources/RealityUI/HasTurnTouch.swift; sourceTree = SOURCE_ROOT; }; F341418D246FF53E006B1ECA /* RealityUI+Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RealityUI+Examples.app"; sourceTree = BUILT_PRODUCTS_DIR; }; F3414190246FF53E006B1ECA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F3414192246FF53E006B1ECA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F3414194246FF540006B1ECA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F3414197246FF540006B1ECA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F3414199246FF540006B1ECA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F361A60C298D57A5006606BC /* RealityUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = RealityUI; path = ..; sourceTree = ""; }; F38598C1247AE28F007BBC88 /* Entity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Entity+Extensions.swift"; sourceTree = ""; }; F3DB9D80247D8BF8006D6CE5 /* ViewController+NonRealityUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+NonRealityUI.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -57,36 +37,20 @@ buildActionMask = 2147483647; files = ( F34141B12471A595006B1ECA /* ShowTime in Frameworks */, + F361A60F298D58A8006606BC /* RealityUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - F33505C82593682F00B8AE86 /* RealityUI */ = { - isa = PBXGroup; - children = ( - F33505CD25936A7700B8AE86 /* HasClick.swift */, - F33505CB25936A7700B8AE86 /* HasRUI.swift */, - F33505D425936A7700B8AE86 /* HasTurnTouch.swift */, - F33505CF25936A7700B8AE86 /* RealityUI.swift */, - F33505D125936A7700B8AE86 /* RUIAnimations.swift */, - F33505CE25936A7700B8AE86 /* RUIButton.swift */, - F33505CA25936A7700B8AE86 /* RUILongTouchGestureRecognizer.swift */, - F33505D225936A7700B8AE86 /* RUISlider.swift */, - F33505D025936A7700B8AE86 /* RUIStepper.swift */, - F33505CC25936A7700B8AE86 /* RUISwitch.swift */, - F33505D325936A7700B8AE86 /* RUIText.swift */, - ); - name = RealityUI; - path = ../../Sources/RealityUI; - sourceTree = ""; - }; F3414184246FF53E006B1ECA = { isa = PBXGroup; children = ( + F361A60B298D57A5006606BC /* Packages */, F341418F246FF53E006B1ECA /* RealityUI+Examples */, F341418E246FF53E006B1ECA /* Products */, + F361A60D298D58A8006606BC /* Frameworks */, ); sourceTree = ""; }; @@ -106,7 +70,6 @@ F301D8A7247A62ED004AE1FA /* ViewController+RealityControls.swift */, F3DB9D80247D8BF8006D6CE5 /* ViewController+NonRealityUI.swift */, F38598C1247AE28F007BBC88 /* Entity+Extensions.swift */, - F33505C82593682F00B8AE86 /* RealityUI */, F3414194246FF540006B1ECA /* Assets.xcassets */, F3414196246FF540006B1ECA /* LaunchScreen.storyboard */, F3414199246FF540006B1ECA /* Info.plist */, @@ -114,6 +77,21 @@ path = "RealityUI+Examples"; sourceTree = ""; }; + F361A60B298D57A5006606BC /* Packages */ = { + isa = PBXGroup; + children = ( + F361A60C298D57A5006606BC /* RealityUI */, + ); + name = Packages; + sourceTree = ""; + }; + F361A60D298D58A8006606BC /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -133,6 +111,7 @@ name = "RealityUI+Examples"; packageProductDependencies = ( F34141B02471A595006B1ECA /* ShowTime */, + F361A60E298D58A8006606BC /* RealityUI */, ); productName = "RealityUI+Examples"; productReference = F341418D246FF53E006B1ECA /* RealityUI+Examples.app */; @@ -145,7 +124,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1140; - LastUpgradeCheck = 1140; + LastUpgradeCheck = 1420; ORGANIZATIONNAME = "Max Cobb"; TargetAttributes = { F341418C246FF53E006B1ECA = { @@ -212,21 +191,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F33505D825936A7700B8AE86 /* HasClick.swift in Sources */, - F33505DD25936A7700B8AE86 /* RUISlider.swift in Sources */, F38598C2247AE28F007BBC88 /* Entity+Extensions.swift in Sources */, - F33505DA25936A7700B8AE86 /* RealityUI.swift in Sources */, F3414193246FF53E006B1ECA /* ViewController.swift in Sources */, - F33505DF25936A7700B8AE86 /* HasTurnTouch.swift in Sources */, F3DB9D81247D8BF8006D6CE5 /* ViewController+NonRealityUI.swift in Sources */, F301D8A8247A62ED004AE1FA /* ViewController+RealityControls.swift in Sources */, - F33505D725936A7700B8AE86 /* RUISwitch.swift in Sources */, - F33505D925936A7700B8AE86 /* RUIButton.swift in Sources */, - F33505DC25936A7700B8AE86 /* RUIAnimations.swift in Sources */, - F33505D525936A7700B8AE86 /* RUILongTouchGestureRecognizer.swift in Sources */, - F33505DE25936A7700B8AE86 /* RUIText.swift in Sources */, - F33505D625936A7700B8AE86 /* HasRUI.swift in Sources */, - F33505DB25936A7700B8AE86 /* RUIStepper.swift in Sources */, F3414191246FF53E006B1ECA /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -271,6 +239,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -331,6 +300,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -364,12 +334,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 278494H572; INFOPLIST_FILE = "RealityUI+Examples/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + PRODUCT_BUNDLE_IDENTIFIER = uk.rocketar.test; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -381,12 +352,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 278494H572; INFOPLIST_FILE = "RealityUI+Examples/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + PRODUCT_BUNDLE_IDENTIFIER = uk.rocketar.test; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -433,6 +405,10 @@ package = F34141AF2471A595006B1ECA /* XCRemoteSwiftPackageReference "ShowTime" */; productName = ShowTime; }; + F361A60E298D58A8006606BC /* RealityUI */ = { + isa = XCSwiftPackageProductDependency; + productName = RealityUI; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = F3414185246FF53E006B1ECA /* Project object */; diff --git a/RealityUI+Examples/RealityUI+Examples/Entity+Extensions.swift b/RealityUI+Examples/RealityUI+Examples/Entity+Extensions.swift index 2b0266d..9d9f2a9 100644 --- a/RealityUI+Examples/RealityUI+Examples/Entity+Extensions.swift +++ b/RealityUI+Examples/RealityUI+Examples/Entity+Extensions.swift @@ -9,24 +9,10 @@ import RealityKit import Foundation import Combine +import RealityUI internal extension Entity { func spin(in axis: SIMD3, duration: TimeInterval, repeats: Bool = true) { - let spun180 = matrix_multiply( - self.transform.matrix, - Transform(scale: .one, rotation: .init(angle: .pi / 2, axis: axis), translation: .zero).matrix - ) - self.move( - to: Transform(matrix: spun180), - relativeTo: self.parent, - duration: duration / 4, - timingFunction: .linear) - var spinCancellable: Cancellable! - spinCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in - spinCancellable.cancel() - if repeats { - self.spin(in: axis, duration: duration, repeats: repeats) - } - }) + self.ruiSpin(by: axis, period: duration, times: -1) } } diff --git a/RealityUI+Examples/RealityUI+Examples/ViewController+NonRealityUI.swift b/RealityUI+Examples/RealityUI+Examples/ViewController+NonRealityUI.swift index 2b89cd8..3d81ead 100644 --- a/RealityUI+Examples/RealityUI+Examples/ViewController+NonRealityUI.swift +++ b/RealityUI+Examples/RealityUI+Examples/ViewController+NonRealityUI.swift @@ -8,7 +8,7 @@ import RealityKit -// import RealityUI + import RealityUI class ContainerCube: Entity, HasPhysicsBody, HasModel { private static var boxPositions: [SIMD3] = [ diff --git a/RealityUI+Examples/RealityUI+Examples/ViewController+RealityControls.swift b/RealityUI+Examples/RealityUI+Examples/ViewController+RealityControls.swift index c3c4731..a2613a4 100644 --- a/RealityUI+Examples/RealityUI+Examples/ViewController+RealityControls.swift +++ b/RealityUI+Examples/RealityUI+Examples/ViewController+RealityControls.swift @@ -10,7 +10,7 @@ import RealityKit import Foundation import Combine -// import RealityUI +import RealityUI class ControlsParent: Entity, HasAnchoring, HasCollision, HasModel, HasTurnTouch { @@ -46,17 +46,14 @@ class ControlsParent: Entity, HasAnchoring, HasCollision, HasModel, HasTurnTouch ) button.transform = Transform( scale: .init(repeating: 0.2), - rotation: simd_quatf(angle: .pi / 2, axis: [1, 0, 0]), - translation: .zero + rotation: simd_quatf(angle: .pi / 2, axis: [1, 0, 0]), translation: .zero ) self.addChild(button) let toggle = RUISwitch(changedCallback: { tog in if tog.isOn { self.tumbler?.spin(in: [0, 0, 1], duration: 3) self.popBoxes(power: 0.1) - } else { - self.tumbler?.stopAllAnimations() - } + } else { self.tumbler?.ruiStopAnim() } }) toggle.transform = Transform( scale: .init(repeating: 0.15), rotation: .init(angle: .pi, axis: [0, 1, 0]), translation: [0, 0.25, -0.25] diff --git a/RealityUI+Examples/RealityUI+Examples/ViewController.swift b/RealityUI+Examples/RealityUI+Examples/ViewController.swift index e6bd6de..bf4e0e7 100644 --- a/RealityUI+Examples/RealityUI+Examples/ViewController.swift +++ b/RealityUI+Examples/RealityUI+Examples/ViewController.swift @@ -10,7 +10,7 @@ import UIKit import RealityKit import ARKit -// import RealityUI +import RealityUI class ViewController: UIViewController { diff --git a/Sources/RealityUI/RUIAnimations.swift b/Sources/RealityUI/RUIAnimations.swift index 5f15e58..41af2d4 100644 --- a/Sources/RealityUI/RUIAnimations.swift +++ b/Sources/RealityUI/RUIAnimations.swift @@ -36,9 +36,9 @@ public extension Entity { Transform(scale: .one, rotation: quat, translation: .zero).matrix ) self.move(to: rockBit, relativeTo: self.parent, duration: period / 2, timingFunction: .easeIn) - var shakeCancellable: Cancellable! - shakeCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in - shakeCancellable.cancel() + let shakeCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in + RealityUI.anims[self]?["shake"]?.cancel() + RealityUI.anims[self]?["shake"] = nil self.shakePrivate( by: simd_quatf(angle: -quat.angle * 2, axis: quat.axis), period: period, @@ -46,29 +46,18 @@ public extension Entity { completion: completion ) }) + if RealityUI.anims[self] == nil { + RealityUI.anims[self] = [:] + } + RealityUI.anims[self]?["shake"] = shakeCancellable } /// Stop all animations on an object, not letting any slip through the net. - /// - Parameters: - /// - tryfor: How long (in nanoseconds) to keep calling `stopAllAnimations()`. Default is 1e8 (0.1s) - /// - completion: Action to take place once the last call to stopAllAnimations has run. - /// - /// Because these animations aren't completely native, there are a few tricks to get them to work, - /// I've found that sometimes the call to stop has been made on the same frame a new animation is starting. - /// Therefor it's sometimes necessary to use this method as a last resort. **Yes, very hacky, please don't judge me.** - func ruiStopAnim(tryfor: UInt64 = UInt64(1e8), completion: ((Entity) -> Void)? = nil) { + /// A static property of RealityUI stores all animations, as well as a reference to the entity. + func ruiStopAnim() { self.stopAllAnimations() - if tryfor > 0 { - let startTime = DispatchTime.now().uptimeNanoseconds - var updCancellable: Cancellable! - updCancellable = self.scene?.subscribe(to: SceneEvents.Update.self, { _ in - self.stopAllAnimations() - if DispatchTime.now().uptimeNanoseconds - startTime > tryfor { - updCancellable.cancel() - completion?(self) - } - }) - } + RealityUI.anims[self]?.forEach { $0.value.cancel() } + RealityUI.anims.removeValue(forKey: self) } private func spinPrivate( @@ -85,15 +74,20 @@ public extension Entity { relativeTo: self.parent, duration: period / 3, timingFunction: times == 0 ? .easeOut : .linear) - var spinCancellable: Cancellable! - spinCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in - spinCancellable.cancel() + let spinCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in + RealityUI.anims[self]?["spin"]?.cancel() + RealityUI.anims[self]?.removeValue(forKey: "spin") if times != 0 { self.spinPrivate(by: axis, period: period, times: max(-1, times - 1), completion: completion) } else { completion?() + if RealityUI.anims[self]?.count == 0 { RealityUI.anims.removeValue(forKey: self) } } }) + if RealityUI.anims[self] == nil { + RealityUI.anims[self] = [:] + } + RealityUI.anims[self]?["spin"] = spinCancellable } private func shakePrivate( @@ -117,14 +111,17 @@ public extension Entity { ) var shakeCancellable: Cancellable! shakeCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in - shakeCancellable.cancel() + RealityUI.anims[self]?["shake"]?.cancel() + RealityUI.anims[self]?.removeValue(forKey: "shake") if remaining != 0 { let newQuat = simd_quatf(angle: -quat.angle, axis: quat.axis) self.shakePrivate(by: newQuat, period: period, remaining: remaining - 1, completion: completion) } else { completion?() + if RealityUI.anims[self]?.count == 0 { RealityUI.anims.removeValue(forKey: self) } } }) + RealityUI.anims[self]?["shake"] = shakeCancellable } } diff --git a/Sources/RealityUI/RUISlider.swift b/Sources/RealityUI/RUISlider.swift index c502f19..3cf3fd1 100644 --- a/Sources/RealityUI/RUISlider.swift +++ b/Sources/RealityUI/RUISlider.swift @@ -331,6 +331,7 @@ public extension HasSlider { private func updateFill(to position: SIMD3, animated: Bool) { guard let fthread = self.findEntity(named: "fill") else {return} + fthread.stopAllAnimations() var threadTransform = fthread.transform threadTransform.scale.x = self.value threadTransform.translation = position @@ -343,6 +344,7 @@ public extension HasSlider { private func updateEmpty(to position: SIMD3, animated: Bool) { guard let fthread = self.getModel(part: .empty) else {return} + fthread.stopAllAnimations() var threadTransform = fthread.transform threadTransform.scale.x = 1 - self.value threadTransform.translation = position diff --git a/Sources/RealityUI/RealityUI.swift b/Sources/RealityUI/RealityUI.swift index 2e27240..3665e63 100644 --- a/Sources/RealityUI/RealityUI.swift +++ b/Sources/RealityUI/RealityUI.swift @@ -19,7 +19,7 @@ import Combine /// RealityUI contains some properties for RealityUI to run in your application. /// ![RealityUI Banner](https://repository-images.githubusercontent.com/265939509/77c8eb00-a362-11ea-995e-482183f9acbd) @objc public class RealityUI: NSObject { - private var componentsRegistered = false + internal var componentsRegistered = false /// Registers RealityUI's component types. Call this before creating any RealityUI classes to avoid issues. /// This method will be automatically called when `ARView.enableRealityUIGestures(_:)` is called, @@ -35,6 +35,8 @@ import Combine /// Mask to exclude entities from being hit by the tap gesture. public static var tapGestureMask: CollisionGroup = .all + /// Store all the RealityUI Animations for an Entity. It's important for memory management that this is empty when it should be. + internal static var anims: [Entity: [String: Cancellable]] = [:] /// Use this to add GestureRecognisers for different RealityUI elements in your scene. /// You do not need multiple GestureRecognisers for multiple elements in the scene. /// - Parameters: @@ -57,6 +59,7 @@ import Combine for comp in RealityUI.RUIComponents { comp.registerComponent() } + self.componentsRegistered = true } /// Different type of gestures used by RealityUI and set to an ARView object. diff --git a/Tests/RealityUITests/RUIAnimationTests.swift b/Tests/RealityUITests/RUIAnimationTests.swift new file mode 100644 index 0000000..ba55339 --- /dev/null +++ b/Tests/RealityUITests/RUIAnimationTests.swift @@ -0,0 +1,84 @@ +// +// RUIAnimationTests.swift +// +// +// Created by Max Cobb on 30/01/2023. +// + +import XCTest +import RealityKit +@testable import RealityUI + +#if os(iOS) +final class RUIAnimationTests: XCTestCase { + + var gestureRecognizer: RUILongTouchGestureRecognizer! + var arView: ARView! + var entity: Entity! + + override func setUpWithError() throws { + let viewC = UIViewController() + arView = ARView(frame: .init(origin: .zero, size: CGSize(width: 256, height: 256))) + viewC.view.addSubview(arView) + entity = Entity() + let anchor = AnchorEntity() + let cam = PerspectiveCamera() + cam.look(at: .zero, from: [0, 1, 1], relativeTo: nil) + anchor.addChild(cam) + anchor.addChild(entity) + arView.scene.addAnchor(anchor) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testRuiSpin() { + let expectation = XCTestExpectation(description: "Spin animation completed") + + print(entity.orientation.angle) + entity.ruiSpin(by: [0, 1, 0], period: 0.3, times: 1) { + expectation.fulfill() + } + print(entity.orientation.angle) + + wait(for: [expectation], timeout: 0.4) + print(entity.orientation.angle) + XCTAssertEqual(RealityUI.anims.count, 0) + } + + func testRuiShake() { + let expectation = XCTestExpectation(description: "Spin animation completed") + + print(entity.orientation.angle) + entity.ruiShake(by: simd_quatf(angle: .pi / 2, axis: [0, 0, 1]), period: 0.25, times: 1) { + expectation.fulfill() + } + // just over 2x the period, as the first and last half period are always added. + wait(for: [expectation], timeout: 0.55) + print(entity.orientation.angle) + // calling stop when there are no animations running + entity.ruiStopAnim() + entity.orientation = .init(angle: .zero, axis: [0, 1, 0]) + XCTAssertEqual(RealityUI.anims.count, 0) + } + + func testRuiStopAnims() { + let expectation = XCTestExpectation(description: "Spin animation completed") + expectation.isInverted = true + print(entity.orientation.angle) + entity.ruiSpin(by: [0, 1, 0], period: 0.3, times: 1) { + expectation.fulfill() + } + print(entity.orientation.angle) + XCTAssertEqual(RealityUI.anims.count, 1) + + wait(for: [expectation], timeout: 0.17) + entity.ruiStopAnim() + XCTAssertEqual(entity.orientation.angle, .pi, accuracy: 0.5) + entity.orientation = .init(angle: .zero, axis: [0, 1, 0]) + XCTAssertEqual(RealityUI.anims.count, 0) + } + +} +#endif diff --git a/Tests/RealityUITests/RUIButtonTests.swift b/Tests/RealityUITests/RUIButtonTests.swift index 6d35d94..fc11355 100644 --- a/Tests/RealityUITests/RUIButtonTests.swift +++ b/Tests/RealityUITests/RUIButtonTests.swift @@ -66,9 +66,7 @@ final class RUIButtonTests: XCTestCase { func testCancelReleaseButton() { let expectation = self.expectation(description: "touchUpCompleted callback was not called") expectation.isInverted = true - button.touchUpCompleted = { _ in - expectation.fulfill() - } + button.touchUpCompleted = { _ in expectation.fulfill() } button.arTouchStarted(SIMD3(0, 0, 0), hasCollided: true) button.arTouchCancelled() waitForExpectations(timeout: 0.1, handler: nil) diff --git a/Tests/RealityUITests/RUISliderTests.swift b/Tests/RealityUITests/RUISliderTests.swift index 073a210..fe72c5a 100644 --- a/Tests/RealityUITests/RUISliderTests.swift +++ b/Tests/RealityUITests/RUISliderTests.swift @@ -52,9 +52,9 @@ final class RUISliderTests: XCTestCase { slider.arTouchCancelled() XCTAssertEqual(startValue, slider.value) } - + #if os(iOS) func testAnimateSliderThumbPos() { - let arView = ARView(frame: CoreFoundation.CGRect(origin: .zero, size: CGSize(width: 256, height: 256))) + let arView = ARView(frame: CGRect(origin: .zero, size: CGSize(width: 256, height: 256))) let anchor = AnchorEntity(world: .zero) let slider = RUISlider(length: 10, start: 0.5) anchor.addChild(slider) @@ -63,7 +63,7 @@ final class RUISliderTests: XCTestCase { slider.setPercent(to: 0.9, animated: true) var expectation = self.expectation(description: "wait for it") expectation.isInverted = true - waitForExpectations(timeout: 0.3, handler: nil) + waitForExpectations(timeout: 0.5, handler: nil) var xpos = slider.getModel(part: "thumb")!.position.x XCTAssertEqual(xpos, -4, accuracy: 0.0005) @@ -74,5 +74,5 @@ final class RUISliderTests: XCTestCase { xpos = slider.getModel(part: "thumb")!.position.x XCTAssertEqual(xpos, 3, accuracy: 0.0005) } - + #endif } diff --git a/Tests/RealityUITests/RUISwitchTests.swift b/Tests/RealityUITests/RUISwitchTests.swift index e7a8836..670349b 100644 --- a/Tests/RealityUITests/RUISwitchTests.swift +++ b/Tests/RealityUITests/RUISwitchTests.swift @@ -30,6 +30,16 @@ final class RUISwitchTests: XCTestCase { XCTAssertTrue(switchChangedCalled) } + func testSwitchRespondsToLighting() { + let testSwitch = RUISwitch() + let unlitMat: Material! = testSwitch.getModel(part: "thumb")?.model?.materials.first + XCTAssertTrue(unlitMat is UnlitMaterial) + testSwitch.respondsToLighting = true + let lightingMat: Material! = testSwitch.getModel(part: "thumb")?.model?.materials.first + XCTAssertTrue(lightingMat is SimpleMaterial) + } + + #if os(iOS) func testSwitchOnOffColors() { let testSwitch = RUISwitch(switchness: SwitchComponent(onColor: .white, offColor: .black)) let arView = ARView(frame: CoreFoundation.CGRect(origin: .zero, size: CGSize(width: 256, height: 256))) @@ -47,26 +57,30 @@ final class RUISwitchTests: XCTestCase { } testSwitch.setOn(true) XCTAssertTrue(testSwitch.isOn) - let expectation = self.expectation(description: "touchUpCompleted callback was not called") + var expectation = self.expectation(description: "touchUpCompleted callback was not called") expectation.isInverted = true waitForExpectations(timeout: 0.3, handler: nil) XCTAssertLessThan(testSwitch.getModel(part: "thumb")!.position.x, 0) guard let onMat = testSwitch.getModel(part: "background")?.model!.materials[0] as? UnlitMaterial else { return XCTFail("Cannot get background material") } - if #available(iOS 15.0, *) { + if #available(macOS 12.0, iOS 15.0, *) { XCTAssertNotEqual(bgMat.color.tint, onMat.color.tint) } testSwitch.setOn(false) + expectation = self.expectation(description: "wait for anim") + expectation.isInverted = true + + waitForExpectations(timeout: 0.3, handler: nil) XCTAssertGreaterThan(testSwitch.getModel(part: "thumb")!.position.x, 0) XCTAssertFalse(testSwitch.isOn) guard let offMat = testSwitch.getModel(part: "background")?.model!.materials[0] as? UnlitMaterial else { return XCTFail("Cannot get background material") } - if #available(iOS 15.0, *) { + if #available(iOS 15.0, macOS 12.0, *) { XCTAssertEqual(bgMat.color.tint, offMat.color.tint) } print("break") } - + #endif } diff --git a/Tests/RealityUITests/RealityUIGeneralTests.swift b/Tests/RealityUITests/RealityUIGeneralTests.swift new file mode 100644 index 0000000..ad74cee --- /dev/null +++ b/Tests/RealityUITests/RealityUIGeneralTests.swift @@ -0,0 +1,52 @@ +// +// RealityUIGeneralTests.swift +// +// +// Created by Max Cobb on 31/01/2023. +// + +import XCTest +import RealityKit +@testable import RealityUI + +final class RealityUIGeneralTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testRealityUILog() throws { + RealityUI.registerComponents() + XCTAssertNotNil(RealityUI.shared) + XCTAssertTrue(RealityUI.shared.componentsRegistered) + } + + func testAddGestures() throws { + let arView = ARView() + RealityUI.enableGestures(.tap, on: arView) + guard let gesturesForView = RealityUI.shared.enabledGestures[arView] else { + return XCTFail("No gestures found in RealityUI") + } + #if os(iOS) + guard let viewGestures = arView.gestureRecognizers else { + return XCTFail("No gestures found in ARView gestureRecognizers") + } + #else + let viewGestures = arView.gestureRecognizers + #endif + XCTAssertTrue(gesturesForView.contains(.tap)) + #if os(iOS) + XCTAssertEqual(viewGestures.count, 2) + XCTAssertTrue(viewGestures[1] is UITapGestureRecognizer) + #else + XCTAssertEqual(viewGestures.count, 1) + XCTAssertTrue(viewGestures[0] is NSClickGestureRecognizer) + #endif + + } + +}