I'm currently struggling to play the animation of a gltf-file with Qt3D/QML.
Model I want to use:
https://sketchfab.com/3d-models/plane-cedc8a07370747f7b0d14400cdf2faf9
Code I have so far:
Entity {
id: root
Transform {
id: planeTransform
scale: 0.1
}
components: [
planeClip,
planeTransform,
planeScene
]
SceneLoader{
id:planeScene
source: "qrc:/Modells/3DModelle/plane/scene.gltf"
}
AnimationClipLoader{
id: planeClipLoader
source: "qrc:/Modells/3DModelle/plane/scene.gltf"
}
ClipAnimator{
id: planeClip
clip: planeClipLoader
channelMapper: ChannelMapper {
mappings: [ ] // Dont know where to get this
}
loops: Animation.Infinite
running: true
}
}
I use the SceneLoader to check if the gltf can be even loaded (it works, I can see the plane).
But I'm struggling to start the animation because I don't know where I get the ChannelMapper from.
When I open the file with 3DViewer on Windows, it plays the Animation correctly, so I suppose it should work without much insider knowledge of the file itself.
Anyone here familiar with playing gltf animations?
Thanks for the help!
To do this you need to traverse the tree of Entity(s) build by SceneLoader and find the appropriate component to initialize ClipAnimator with.
It's going to look something like this:
SceneLoader {
id: planeScene
source: "qrc:/Modells/3DModelle/plane/scene.gltf"
onStatusChanged: {
console.log("SceneLoader status=" + status);
if (status != SceneLoader.Ready)
return;
// retrieve a list of all the Entity(s) in the scene
var entityNames = planeScene.entityNames();
console.log("SceneLoader: entityNames=" + entityNames);
// traverse the Entity tree
for (var i = 0; i < entityNames.length; ++i) {
var entityName = entityNames[i];
var entityVar = planeScene.entity(entityName);
console.log("SceneLoader: entity=" + entityName + " components.length=" + entityVar.components.length);
// iterate on the components of this Entity
for (var j = 0; j < entityVar.components.length; ++j) {
var cmp = entityVar.components[j];
if (!cmp)
continue;
var cmp_class = cmp.toString();
console.log("SceneLoader: -> " + cmp_class);
// compare the type of this component to whatever type
// you are looking for. On this silly example, its QPhongMaterial:
if (cmp_class.indexOf("QPhongMaterial") >= 0) {
// initialize ClipAnimator with this data
}
}
}
}
}
I'm sharing the generic procedure here as I don't have a way to test this right now and figure out the data type you need to initialize ClipAnimator correctly.
I have a button in SwiftUI and I would like to be able to have a different action for "tap button" (normal click/tap) and "long press".
Is that possible in SwiftUI?
Here is the simple code for the button I have now (handles only the "normal" tap/touch case).
Button(action: {self.BLEinfo.startScan() }) {
Text("Scan")
} .disabled(self.BLEinfo.isScanning)
I already tried to add a "longPress gesture" but it still only "executes" the "normal/short" click. This was the code I tried:
Button(action: {self.BLEinfo.startScan() }) {
Text("Scan")
.fontWeight(.regular)
.font(.body)
.gesture(
LongPressGesture(minimumDuration: 2)
.onEnded { _ in
print("Pressed!")
}
)
}
Thanks!
Gerard
I tried many things but finally I did something like this:
Button(action: {
}) {
VStack {
Image(self.imageName)
.resizable()
.onTapGesture {
self.action(false)
}
.onLongPressGesture(minimumDuration: 0.1) {
self.action(true)
}
}
}
It is still a button with effects but short and long press are different.
Combining a high priority gesture and a simultaneous gesture should do the trick.
Button(action: {})
{
Text("A Button")
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
print("Loooong")
}
)
.highPriorityGesture(TapGesture()
.onEnded { _ in
print("Tap")
})
Found this a handy pattern when interacting with other views as well.
I just discovered that the effect depends on the order of the implementation. Implementing the detection of gestures in the following order it seems to be possible to detect and identify all three gestures:
handle a double tap gesture
handle a longPressGesture
handle a single tap gesture
Tested on Xcode Version 11.3.1 (11C504)
fileprivate func myView(_ height: CGFloat, _ width: CGFloat) -> some View {
return self.textLabel(height: height, width: width)
.frame(width: width, height: height)
.onTapGesture(count: 2) {
self.action(2)
}
.onLongPressGesture {
self.action(3)
}
.onTapGesture(count: 1) {
self.action(1)
}
}
Here is my implementation using a modifier:
struct TapAndLongPressModifier: ViewModifier {
#State private var isLongPressing = false
let tapAction: (()->())
let longPressAction: (()->())
func body(content: Content) -> some View {
content
.scaleEffect(isLongPressing ? 0.95 : 1.0)
.onLongPressGesture(minimumDuration: 1.0, pressing: { (isPressing) in
withAnimation {
isLongPressing = isPressing
print(isPressing)
}
}, perform: {
longPressAction()
})
.simultaneousGesture(
TapGesture()
.onEnded { _ in
tapAction()
}
)
}
}
Use it like this on any view:
.modifier(TapAndLongPressModifier(tapAction: { <tap action> },
longPressAction: { <long press action> }))
It just mimics the look a button by scaling the view down a bit. You can put any other effect you want after scaleEffect to make it look how you want when pressed.
I had to do this for an app I am building, so just wanted to share. Refer code at the bottom, it is relatively self explanatory and sticks within the main elements of SwiftUI.
The main differences between this answer and the ones above is that this allows for updating the button's background color depending on state and also covers the use case of wanting the action of the long press to occur once the finger is lifted and not when the time threshold is passed.
As noted by others, I was unable to directly apply gestures to the Button and had to apply them to the Text View inside it. This has the unfortunate side-effect of reducing the 'hitbox' of the button, if I pressed near the edges of the button, the gesture would not fire. Accordingly I removed the Button and focused on manipulating my Text View object directly (this can be replaced with Image View, or other views (but not Button!)).
The below code sets up three gestures:
A LongPressGesture that fires immediately and reflects the 'tap' gesture in your question (I haven't tested but this may be able to replaced with the TapGesture)
Another LongPressGesture that has a minimum duration of 0.25 and reflect the 'long press' gesture in your question
A drag gesture with minimum distance of 0 to allow us to do events at the end of our fingers lifting from the button and not automatically at 0.25 seconds (you can remove this if this is not your use case). You can read more about this here: How do you detect a SwiftUI touchDown event with no movement or duration?
We sequence the gestures as follows: Use 'Exclusively' to combine the "Long Press" (i.e. 2 & 3 above combined) and Tap (first gesture above), and if the 0.25 second threshold for "Long Press" is not reached, the tap gesture is executed. The "Long Press" itself is a sequence of our long press gesture and our drag gesture so that the action is only performed once our finger is lifted up.
I also added code in the below for updating the button's colours depending on the state. One small thing to note is that I had to add code on the button's colour into the onEnded parts of the long press and drag gesture because the minuscule processing time would unfortunately result in the button switching back to darkButton colour between the longPressGesture and the DragGesture (which should not happen theoretically, unless I have a bug somewhere!).
You can read more here about Gestures: https://developer.apple.com/documentation/swiftui/gestures/composing_swiftui_gestures
If you modify the below and pay attention to Apple's notes on Gestures (also this answer was useful reading: How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?) you should be able to set up complex customised button interactions. Use the gestures as building blocks and combine them to remove any deficiency within individual gestures (e.g. longPressGesture does not have an option to do the events at its end and not when the condition is reached).
P.S. I have a global environment object 'dataRouter' (which is unrelated to the question, and just how I choose to share parameters across my swift views), which you can safely edit out.
struct AdvanceButton: View {
#EnvironmentObject var dataRouter: DataRouter
#State var width: CGFloat
#State var height: CGFloat
#State var bgColor: Color
#GestureState var longPress = false
#GestureState var longDrag = false
var body: some View {
let longPressGestureDelay = DragGesture(minimumDistance: 0)
.updating($longDrag) { currentstate, gestureState, transaction in
gestureState = true
}
.onEnded { value in
print(value.translation) // We can use value.translation to see how far away our finger moved and accordingly cancel the action (code not shown here)
print("long press action goes here")
self.bgColor = self.dataRouter.darkButton
}
let shortPressGesture = LongPressGesture(minimumDuration: 0)
.onEnded { _ in
print("short press goes here")
}
let longTapGesture = LongPressGesture(minimumDuration: 0.25)
.updating($longPress) { currentstate, gestureState, transaction in
gestureState = true
}
.onEnded { _ in
self.bgColor = self.dataRouter.lightButton
}
let tapBeforeLongGestures = longTapGesture.sequenced(before:longPressGestureDelay).exclusively(before: shortPressGesture)
return
Text("9")
.font(self.dataRouter.fontStyle)
.foregroundColor(self.dataRouter.darkButtonText)
.frame(width: width, height: height)
.background(self.longPress ? self.dataRouter.lightButton : (self.longDrag ? self.dataRouter.brightButton : self.bgColor))
.cornerRadius(15)
.gesture(tapBeforeLongGestures)
}
}
This isn't tested, but you can try to add a LongPressGesture to your button.
It'll presumably look something like this.
struct ContentView: View {
#GestureState var isLongPressed = false
var body: some View {
let longPress = LongPressGesture()
.updating($isLongPressed) { value, state, transaction in
state = value
}
return Button(/*...*/)
.gesture(longPress)
}
}
Kevin's answer was the closest to what I needed. Since ordering the longPressGesture before the tapGesture broke ScrollViews for me, but the inverse made the minimumDuration parameter do nothing, I implemented the long press functionality myself:
struct TapAndLongPressModifier: ViewModifier {
#State private var canTap = false
#State private var pressId = 0
let tapAction: (()->())
let longPressAction: (()->())
var minimumDuration = 1.0
func body(content: Content) -> some View {
content
.onTapGesture {
if canTap {
tapAction()
}
}
.onLongPressGesture(
minimumDuration: 1.0,
pressing: { (isPressing) in
pressId += 1
canTap = isPressing
if isPressing {
let thisId = pressId
DispatchQueue.main.asyncAfter(deadline: .now() + minimumDuration) {
if thisId == pressId {
canTap = false
longPressAction()
}
}
}
},
// We won't actually use this
perform: {}
)
}
}
just do this:
the first modifier should be onLongPressGesture(minumumDuration: (the duration you want))
and the following mondifier should be onLongPressGesture(minimumDuration: 0.01) <- or some other super small numbers
this works for me perfectly
As a follow up, I had the same issue and I tried all of these answers but didn't like how they all worked.
I ended up using a .contextMenu it was way easier and produces pretty much the same effect.
Check link here
and here is an example
UPDATE:
As of iOS 16, .contextMenu has been depreciated. So I ended up using .simultaneousGesture on the Button, not the content in the button's label block.
i.e.
Button {
// handle Button Tap
} label: {
// button label content here
}
.simultaneousGesture(LongPressGesture()
.onEnded { _ in
// handle long press here
}
)
This still preserves the button animations as well.
Note tested before iOS 16 however.
Thought I'd post back on this, in case anyone else is struggling. Strange that Apple's default behaviour works on most controls but not buttons. In my case I wanted to keep button effects while supporting long press.
An approach that works without too much complexity is to ignore the default button action and create a simultaneous gesture that handles both normal and long clicks.
In your view you can apply a custom long press modifier like this:
var body: some View {
// Apply the modifier
Button(action: self.onReloadDefaultAction) {
Text("Reload")
}
.modifier(LongPressModifier(
isDisabled: self.sessionButtonsDisabled,
completionHandler: self.onReloadPressed))
}
// Ignore the default click
private func onReloadDefaultAction() {
}
// Handle the simultaneous gesture
private func onReloadPressed(isLongPress: Bool) {
// Do the work here
}
My long press modifier implementation looked like this and uses the drag gesture that I found from another post. Not very intuitive but it works reliably, though of course I would prefer not to have to code this plumbing myself.
struct LongPressModifier: ViewModifier {
// Mutable state
#State private var startTime: Date?
// Properties
private let isDisabled: Bool
private let longPressSeconds: Double
private let completionHandler: (Bool) -> Void
// Initialise long press behaviour to 2 seconds
init(isDisabled: Bool, completionHandler: #escaping (Bool) -> Void) {
self.isDisabled = isDisabled
self.longPressSeconds = 2.0
self.completionHandler = completionHandler
}
// Capture the start and end times
func body(content: Content) -> some View {
content.simultaneousGesture(DragGesture(minimumDistance: 0)
.onChanged { _ in
if self.isDisabled {
return
}
// Record the start time at the time we are clicked
if self.startTime == nil {
self.startTime = Date()
}
}
.onEnded { _ in
if self.isDisabled {
return
}
// Measure the time elapsed and reset
let endTime = Date()
let interval = self.startTime!.distance(to: endTime)
self.startTime = nil
// Return a boolean indicating whether a normal or long press
let isLongPress = !interval.isLess(than: self.longPressSeconds)
self.completionHandler(isLongPress)
})
}
}
Try this :)
Handles isInactive, isPressing, isLongPress and Tap(Click)
based on this
I tried to make this as a viewmodifier without success. I would like to see an example with #GestureState variable wrapper used in same manner as #State/#Published are bound to #Binding in view components.
Tested: Xcode 12.0 beta, macOS Big Sur 11.0 beta
import SwiftUI
enum PressState {
case inactive
case pressing
case longPress
var isPressing: Bool {
switch self {
case .inactive:
return false
case .pressing, .longPress:
return true
}
}
var isLongPress: Bool {
switch self {
case .inactive, .pressing:
return false
case .longPress:
return true
}
}
var isInactive : Bool {
switch self {
case .inactive:
return true
case .pressing, .longPress:
return false
}
}
}
struct ContentView: View {
#GestureState private var pressState: PressState = PressState.inactive
#State var showClick: Bool = false
var press: some Gesture {
LongPressGesture(minimumDuration: 0.8, maximumDistance: 50.0)
.sequenced(before: LongPressGesture(minimumDuration: .infinity, maximumDistance: 50.0))
.updating($pressState) { value, state, transaction in
switch value {
case .first(true): // first gesture starts
state = PressState.pressing
case .second(true, nil): // first ends, second starts
state = PressState.longPress
default: break
}
}
}
var body: some View {
ZStack{
Group {
Text("Click")
.offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -120 : -100) : -40)
.animation(Animation.linear(duration: 0.5))
.opacity(showClick ? 1 : 0 )
.animation(Animation.linear(duration: 0.3))
Text("Pressing")
.opacity(pressState.isPressing ? 1 : 0 )
.offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -100 : -80) : -20)
.animation(Animation.linear(duration: 0.5))
Text("Long press")
.opacity(pressState.isLongPress ? 1 : 0 )
.offset(x: 0, y: pressState.isLongPress ? -80 : 0)
.animation(Animation.linear(duration: 0.5))
}
Group{
Image(systemName: pressState.isLongPress ? "face.smiling.fill" : (pressState.isPressing ? "circle.fill" : "circle"))
.offset(x: 0, y: -100)
.font(.system(size: 60))
.opacity(pressState.isLongPress ? 1 : (pressState.isPressing ? 0.6 : 0.2))
.foregroundColor(pressState.isLongPress ? .orange : (pressState.isPressing ? .yellow : .white))
.rotationEffect(.degrees(pressState.isLongPress ? 360 : 0), anchor: .center)
.animation(Animation.linear(duration: 1))
Button(action: {
showClick = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
self.showClick = false
})
}, label: {
ZStack {
Circle()
.fill(self.pressState.isPressing ? Color.blue : Color.orange)
.frame(width: 100, height: 100, alignment: .center)
Text("touch me")
}}).simultaneousGesture(press)
}.offset(x: 0, y: 110)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have a ListView, where the currently displayed modelData changes as a button cycles through several department options. If one of these departments has no data, my delegate continues showing the previous list data until it reaches a new section of modelData with data.
What I want to do, is when the model is 'empty' (being undefined, which may happen when the key it's looking for is yet to be created in my Firebase Database, or no items are currently visible), text/image is shown instead; i.e, "Move along now, nothing to see here".
My model is drawn from JSON, an example is below. and my calendarUserItems is the root node of multiple children within my Firebase Database, the aim of my AppButton.groupCycle was too add a further direction to each child node, filtering the data by this to view and edit within the page.
A sample of my code is:
Page {
id: adminPage
property var departments: [1,2,3,4]
property int currGroupIndex: 0
AppButton {
id: groupCycle
text: "Viewing: " + departments[currGroupIndex]
onClicked: {
if (currGroupIndex == departments.length - 1)
currGroupIndex = 0;
else
currGroupIndex++;
}
}
ListView {
model: Object.keys(dataModel.calendarUserItems[departments[currGroupIndex]])
delegate: modelData.visible ? currentGroupList : emptyHol
Component {
id: emptyHol
AppText {
text: "nothing to see here move along now!"
}
}
Component {
id: currentGroupList
SimpleRow {
id: container
readonly property var calendarUserItem: dataModel.calendarUserItems[departments[currGroupIndex]][modelData] || {}
visible: container.calendarUserItem.status === "pending" ? true : false
// only pending items visible
// remaining code for simple row
}
}
}
}
an example of JSON within my dataModel.calendarUserItems is:
"groupName": [
{ "department1":
{ "1555111624727" : {
"creationDate" : 1555111624727,
"date" : "2019-03-15T12:00:00.000",
"name" : "Edward Lawrence",
"status": "pending"
},
//several of these entries within department1
},
},
{ "department2":
{ "1555111624727" : {
"creationDate" : 1555111624456,
"date" : "2019-05-1T12:00:00.000",
"name" : "Katie P",
"status": 1
},
//several of these entries within department2
},
}
//departments 3 & 4 as the same
]
If departments 2 and 3 have modelData, yet 1 and 4 do not, I want the text to display instead, and the ListView emptied, instead of showing the previous modelData.
I have tried playing with the image/text visibility but the issue lays more with clearing the modelData and I'm unsure where to begin?
Any help is greatly appreciated!
I have achieved the display by using the following as my delegate:
delegate: {
if (!(departments[currGroupIndex] in dataModel.calendarUserItems) ) {
return emptyHol;
}
var subgroups = Object.keys(dataModel.calendarUserItems[departments[currGroupIndex]]);
for (var i in subgroups) {
var subgroup = dataModel.calendarUserItems[departments[currGroupIndex]][subgroups[i]];
modelArr.push(subgroup);
}
var modelObect = modelArr.find( function(obj) { return obj.status === "pending"; } );
if (modelObect === undefined) {
return emptyHol;
}
return currentGroupList;
}
Then when my AppButton.groupCycle is pressed, I have added modelArr = [] to clear the array on each press, this works as intended.
Thanks!
I am trying to implement a keyClick with a shift modifier but it doesn't work. Below is a basic setup of what I am trying to do. The first test_case1 can perform what I am doing but I'd like the second test_case2 to work as well but with using the Qt.ShiftModifier.
../MyTextBox.qml
Page {
id: page1
objectName: "page1"
TextField {
id: lastNameField
objectName: "lastNameField"
text: qsTr("")
}
}
tst_page.qml
import "../"
Item {
width: 800
height: 600
MyTextBox {
id: page1
}
TestCase {
id: "txtBox"
when: windowShown
function test_case1 () {
//var qmlObj = findChild(page1, "lastNameField")
var qmlObj = page1.lastNameField
// Bring to focus
mouseClick(qmlObj, Qt.LeftButton, Qt.NoModifier)
// Keypress
keyPress("Y")
keyPress("e")
keyPress("s")
tryCompare(qmlObj, "text", "Yes") // pass
}
function test_case2 () {
var qmlObj = page1.lastNameField
// Bring to focus
mouseClick(qmlObj, Qt.LeftButton, Qt.NoModifier)
// Keypress
keyClick(QT.Key_Y, Qt.ShiftModifier)
keyClick(QT.Key_E)
keyClick(QT.Key_S)
tryCompare(qmlObj, "text", "Yes") // fail
}
}
}
test output
PASS : test_case1()
FAIL! : test_case2()
Actual (): yes
Expected (): Yes
Edit: Added a simple project to github for testing.
In react-native doc, it says to check UIExploreIntegrationTest. It seems that it requires some setup on Xcode as it uses Objective C code(*.m). I'm new on Obj-C test.. May I know how to set up the integration test on Xcode?
With some guesswork I was able to nail down a few steps to get integration tests running on iOS. However I'm still figuring out how to get Android integration tests working.
Go ahead and copy IntegrationTests.js from the RN github and make a new JS file called Tests.js
Place both of these files in the root of your project. Then change IntegrationTests.js by going down and changing all of their requires to just one require statement for the file you just created require('./Tests')
Here is a basic implementation of what your Tests.js file should look like:
'use strict';
var React = require('react');
var ReactNative = require('react-native');
var {
Text,
View,
} = ReactNative;
var { TestModule } = ReactNative.NativeModules;
var Tests = React.createClass({
shouldResolve: false,
shouldReject: false,
propTypes: {
RunSampleCall: React.PropTypes.bool
},
getInitialState() {
return {
done: false,
};
},
componentDidMount() {
if(this.props.TestName === "SomeTest"){
Promise.all([this.SomeTest()]).then(()=>
{
TestModule.markTestPassed(this.shouldResolve);
});
return;
}
},
async SomeTest(){
var one = 1;
var two = 2;
var three = one + two;
if(three === 3){
this.shouldResolve = true;
}else{
this.shouldResolve = false;
}
}
render() : ReactElement<any> {
return <View />;
}
});
Tests.displayName = 'Tests';
module.exports = Tests;
Here is a basic implementation of your Tests.m file (inside xcode)
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <RCTTest/RCTTestRunner.h>
#import "RCTAssert.h"
#define RCT_TEST(name) \
- (void)test##name \
{ \
[_runner runTest:_cmd module:##name]; \
}
#interface IntegrationTests : XCTestCase
#end
#implementation IntegrationTests
{
RCTTestRunner *_runner;
}
- (void)setUp
{
_runner = RCTInitRunnerForApp(#"IntegrationTests", nil);
}
- (void)test_SomeTest
{
[_runner runTest:_cmd
module:#"Tests"
initialProps:#{#"TestName": #"SomeTest"}
configurationBlock:nil];
}
#end
Also you need to add RCTTest from node_modules/react-native/Libraries/RCTTest/RCTTest.xcodeproj to your libraries. then you need to drag the product libRCTTest.a of that project you added to Linked Frameworks and Libraries for your main target in the general tab.
^^ that path might be slightly incorrect
Then you need to edit your scheme and set an environment variable CI_USE_PACKAGER to 1
So if you do all those steps you should have a simple test run and pass. It should be fairly easy to expand after that. Sorry if my answer is slightly sloppy, let me know if you have any questions.