diff --git a/.github/workflows/swift-build.yml b/.github/workflows/swift-build.yml
index e25bdb6..7515821 100644
--- a/.github/workflows/swift-build.yml
+++ b/.github/workflows/swift-build.yml
@@ -17,20 +17,11 @@ jobs:
- uses: actions/checkout@v3
- name: Swift Lint
run: swiftlint --strict
- - name: Build Package
- run: |
- 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 RealityUI -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
+ xcodebuild build -scheme RealityUI -destination "platform=macOS" | xcpretty
env:
- PROJECT: RealityUI.xcodeproj
- SCHEME: RealityUI-Package
- SDK: macosx
+ SCHEME: RealityUI
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/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/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/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/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/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
new file mode 100644
index 0000000..fc11355
--- /dev/null
+++ b/Tests/RealityUITests/RUIButtonTests.swift
@@ -0,0 +1,81 @@
+//
+// 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..f219b22
--- /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)
+internal 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
diff --git a/Tests/RealityUITests/RUILongTouchSliderTests.swift b/Tests/RealityUITests/RUILongTouchSliderTests.swift
new file mode 100644
index 0000000..0a20798
--- /dev/null
+++ b/Tests/RealityUITests/RUILongTouchSliderTests.swift
@@ -0,0 +1,79 @@
+//
+// 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)
+ }
+
+ 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
new file mode 100644
index 0000000..fe72c5a
--- /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
+ let startValue = slider.value
+ slider.arTouchStarted(worldCoordinate)
+ XCTAssertEqual(startValue, 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
+ let startValue = slider.value
+ slider.arTouchStarted(worldCoordinate)
+ XCTAssertEqual(startValue, slider.value)
+ slider.arTouchCancelled()
+ XCTAssertEqual(startValue, slider.value)
+ }
+ #if os(iOS)
+ func testAnimateSliderThumbPos() {
+ 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)
+ 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.5, 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)
+ }
+ #endif
+}
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..670349b
--- /dev/null
+++ b/Tests/RealityUITests/RUISwitchTests.swift
@@ -0,0 +1,86 @@
+//
+// 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 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)))
+ 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)
+ 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(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, macOS 12.0, *) {
+ XCTAssertEqual(bgMat.color.tint, offMat.color.tint)
+ }
+ print("break")
+ }
+ #endif
+}
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)
+ }
+
+}
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
+
+ }
+
+}