CAMetalLayer.nextDrawable takes much time, even more than 8ms - ios

CAMetalLayer.nextDrawable() should not be a very time-comsuming method.But sometimes it often takes much time, even more than 8ms
Copy the code below
Follow the comment guide in viewDidLoad
See the log print
class TestVC: UIViewController {
let metalLayer:CAMetalLayer = CAMetalLayer()
var displayLink:CADisplayLink!
var thread:Thread!
var animationView:AnimationView!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.displayLink.isPaused = true
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.displayLink.isPaused = false
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("ahhhaa")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
let scale = UIScreen.main.scale
metalLayer.frame = self.view.bounds
metalLayer.drawableSize = CGSize(width: self.view.frame.width * scale, height: self.view.frame.height * scale)
self.view.layer.addSublayer(metalLayer)
self.addDisplayLinkInUITaskRunner()
// Wait screen loading complete,and next,You can:
// 1. Swipe down the control center, you will see a lot log print in console, and it will
// constantly exists.
// 2. Move the app to bg, and then move to fg, you will see a lot of log,
// it also will constantly exist for a long time.
// 3. Just don't do anything, you might see it the log print constantly....
// And when you tap the screen, it may disappear, and After a while, you may see log again....
}
func addDisplayLinkInUITaskRunner() {
self.thread = Thread(block: {
RunLoop.current.add(NSMachPort(), forMode: .common)
RunLoop.current.run()
})
self.thread.name = ""
self.thread.start()
self.perform(#selector(addDisplayLink), on: thread, with: nil, waitUntilDone: false)
}
#objc func addDisplayLink() {
self.displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLink))
if #available(iOS 15.0, *) {
self.displayLink.preferredFrameRateRange = .init(minimum: 60, maximum: 120, preferred: 120)
} else {
self.displayLink.preferredFramesPerSecond = 120
}
self.displayLink.add(to: .current, forMode: .common)
}
#objc private func onDisplayLink() {
let startTime = CACurrentMediaTime()
let frameDrawable = metalLayer.nextDrawable()!
let timeUsed = CACurrentMediaTime() - startTime
// If time used to get next drawble over 3ms,
// we print it here to indicate this method take much time!
if (timeUsed > 0.005) {
print("CAMetalLayer.nextDrawable take much time!! -> \(String(format: "%.2f", timeUsed * 1000)) ms")
}
frameDrawable.present()
}
}

If you have displaySyncEnabled set to true (the default) it will wait for the next vsync to display the drawable. This means you can very quickly run out of drawables, and so nextDrawable will wait until one becomes available (or up to 1 second).
In other words, since you already have drawables queued to be presented, you can't get another one until one actually becomes available.
If you indeed want to present them as fast as possible - set displaySyncEabled to false. However, the current behavior may just be what you want, since you rarely gain anything from displaying frames faster than the display's refresh rate.

Related

How to fix timer for drawing initiated stopwatch?

Hi there I am completing some research for Alzheimers - I want to be able to record the time it takes to complete a drawing. Both the time spend with apple pencil on tablet and the time spent overall to complete a drawing (time on tablet plus time in between strokes).
I have created this application so far but can't get the timer to work.
I have the drawing/scribble board down pat.
I have tried many different approaches but I am just not experienced enough in code to work out why it is not starting the timer when the apple pencil hits the tablet. The code below is for the ViewController script.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var canvasView: CanvasView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func clearCanvas(_ sender: Any) { //this button will clear canvas and clear timers!
canvasView.clearCanvas()
timer.invalidate()
seconds = 0 //Here we manually enter the restarting point for the seconds, but it would be wiser to make this a variable or constant.
timeDrawing.text = "\(seconds)"
}
#IBOutlet weak var timeDrawing: UILabel! //This is one of the label used to display the time drawing (i have yet to add the label to display the time inbetween strokes)
var seconds = 0
var timer = Timer()
var isTimerRunning = false //This will be used to make sure only one timer is created at a time.
var resumeTapped = false
var touchPoint:CGPoint!
var touches:UITouch!
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
seconds += 1
timeDrawing.text = "\(seconds)" //This will update the label.
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let touches = touch.location(in: canvasView) // or maybe ...(in: self)
if touch.type == .pencil {
if !isTimerRunning {
runTimer()
} else {
timer.invalidate()
}
}
}
}
I was hoping when the apple pencil touched the tablet it would start the timer. And then when the pencil left the tablet it would stop one of the timers. (i have yet to add another timer for the time inbetween strokes, any help with that would be appreciated too.)
As #ElTomato said you're looking for touchesEnded.
You have to move timer.invalidate() from touchesBegan to touchesEnded.
Also you should change your updateTimer logic for more high accuracy.
private(set) var from: Date?
#objc func updateTimer() {
let to = Date()
let timeIntervalFrom = (from ?? to).timeIntervalSince1970
let time = to.timeIntervalSince1970 - timeIntervalFrom
timeDrawing.text = "\(round(time))" //This will update the label.
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
let touches = touch.location(in: canvasView) // or maybe ...(in: self)
if touch.type == .pencil {
if !isTimerRunning {
from = Date()
runTimer()
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if !isTimerRunning {
timer.invalidate()
timer = nil
from = nil
timeDrawing.text = "0"
}
}
Here is how it works for the code below. 1. Drag a UIView control from the object library onto the storyboard. While this control is selected, enter 'TouchView' in the class field under the identity inspector. Make an IBOutlet connection to the view controller, naming it touchView.
import UIKit
class ViewController: UIViewController, TouchViewDelegate {
// MARK: - Variables
// MARK: - IBOutlet
#IBOutlet weak var touchView: TouchView!
// MARK: - IBAction
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
/* delegate */
touchView.touchViewDelegate = self
}
// MARK: - Protocol
var startDate: Date?
func touchBegan(date: Date) {
startDate = date
}
func touchEnd(date: Date) {
if let myDate = startDate {
let components = Calendar.current.dateComponents([.second], from: myDate, to: date)
if let secs = components.second {
print(secs)
}
}
}
}
import UIKit
protocol TouchViewDelegate: class {
func touchBegan(date: Date)
func touchEnd(date: Date)
}
class TouchView: UIView {
weak var touchViewDelegate: TouchViewDelegate?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = event?.allTouches?.first
if let touchPoint = touch?.location(in: self) {
if self.bounds.contains(touchPoint) {
Swift.print("You have started a session")
touchViewDelegate?.touchBegan(date: Date())
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
Swift.print("You have ended the session.")
touchViewDelegate?.touchEnd(date: Date())
}
}
So when the user touches the touch view, TouchView will make a delegate call to the view controller. When the user gets his finger off off the touch view, it will make another call to the view controller. And the view controller will calculate the difference between two dates. So you don't need a timer. It's my person opinion, but you should stay away from Timer unless you have no alternatives.

How to constantly get the x-position of an moving object in swift?

I have to buttons leftbutton, rightbutton both of them being SKSpriteNode().
When the user touches one of the buttons, there is a little ship that moves left and right as long as the user holds the touch.
Now I need a function or anything else that gives me the ship.position.x the whole time. I am stuck to try to make it print the position constantly. I can make it print everytime the button is touched but it only prints it once.
In my didMove I only created the buttons and the ship. So it should be rather irrelevant.
func moveShip (moveBy: CGFloat, forTheKey: String) {
let moveAction = SKAction.moveBy(x: moveBy, y: 0, duration: 0.09)
let repeatForEver = SKAction.repeatForever(moveAction)
let movingSequence = SKAction.sequence([moveAction, repeatForEver])
ship.run(movingSequence, withKey: forTheKey)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("\(ship.position.x)")
for touch: AnyObject in touches {
let pointTouched = touch.location(in: self)
if leftButton.contains(pointTouched) {
// !! I MAKE IT PRINT THE POSITION HERE !!
moveShip(moveBy: -30, forTheKey: "leftButton")
}
else if rightButton.contains(pointTouched) {
moveShip(moveBy: 30, forTheKey: "rightButton")
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let pos = touch.location(in: self)
let node = self.atPoint(pos)
if node == aButton {
} else {
ship.removeAction(forKey: "leftButton")
ship.removeAction(forKey: "rightButton")
}
}
}
In my code, the position is only printed once at the beginning of the touch and not printed until you release the touch and touch it again. Is there a possible way to do this with my code?
The touchesMoved function won't help you with your particular problem. You can check your frame constantly by creating a var timer = Timer() as instance variable.
You then have to set up the timer and the function that is called when a specific amount of time is over.
Do as following in your didMove function:
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self,
selector: #selector(detectShipPosition), userInfo: nil, repeats: true)
As this will repeat itself every 0.01 seconds it will call the function detectShipPosition which you will implement OUTSIDE didMove.
func detectShipPosition(){
print("\(ship.position.x)")
}
Hi you can make use of the touchesMoved delegate method(It tells the responder when one or more touches associated with an event changed.) as follows,
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("\(ship.position.x)")
}
Solution for the comment you posted on Bharath answer.
You can change below with your own value:
longGesture.minimumPressDuration
You can use UILongPressGestureRecognizer :
class ViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var leftButton: UIButton!
#IBOutlet weak var rightButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.longPress(_:)))
longGesture.minimumPressDuration = 0.5
longGesture.delegate = self
leftButton.addGestureRecognizer(longGesture)
rightButton.addGestureRecognizer(longGesture)
}
#objc func longPress(_ gestureReconizer: UILongPressGestureRecognizer) {
print("\(ship.position.x)")
}
}
If you implement the update() function in your scene, it will be called for every frame and you can check your object's position (and existence) in there to print the value when it changes:
override func update(_ currentTime: TimeInterval)
{
if let ship = theShipSprite,
ship.position != lastPosition
{
print(ship.position) // or whatever it is you need to do
lastPosition = shipPosition
}
}

ARKit takes 30 seconds to start tracking?

Ok, exploring ARKit with Swift in iOS 11 here and I have made a very simple app that just adds nodes at the point where the user taps:
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
let actualScene = SCNScene(named: "art.scnassets/ship.scn")!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let results = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitFeature = results.last else { return }
let hitTransform = SCNMatrix4.init(hitFeature.worldTransform) // <- if higher than beta 1, use just this -> hitFeature.worldTransform
let hitPosition = SCNVector3Make(hitTransform.m41,
hitTransform.m42,
hitTransform.m43)
createBall(hitPosition: hitPosition)
}
func createBall(hitPosition : SCNVector3) {
let newBall = SCNSphere(radius: 0.01)
let newBallNode = SCNNode(geometry: newBall)
newBallNode.position = hitPosition
self.sceneView.scene.rootNode.addChildNode(newBallNode)
}
And this works. My issue is that when first running the app, it takes 30-60 seconds of just panning the camera around where tapping does nothing.
It seems like ARKit is "loading", so that when I tap in this first minute no nodes appear in the tapped position. Nothing happens for that first minute.
Why is this? Is there a way to expedite this loading process? What is happening here?
On the overrided function viewWillAppear call this function
func setUpSceneView() {
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
sceneView.session.run(configuration)
sceneView.delegate = self
}
and delete everything in viewdidload and add what you need in view will appear

Paused layer come unpause after start app from background

I have set play/pause button to gameLayer - here: How to prevent run SKAction on paused scene (after unpaused), texture of node not change after pause/unpause scene
Everything works fine, but I'm helpless with app on background. When I pause gameLayer and app goes to background or when I lock device, after app going to foreground, game layer is automatically unpause. How to prevent this?
Thanks!
So this is what worked for me:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var gameLayer = SKNode()
override func didMove(to view: SKView) {
NotificationCenter.default.addObserver(self,
selector: #selector(GameScene.pause),
name: .UIApplicationDidBecomeActive,
object: nil)
let moveUp = SKAction.moveBy(x: 0, y: 100, duration: 2)
let sequence = SKAction.sequence([moveUp, moveUp.reversed()])
let loop = SKAction.repeatForever(sequence)
let sprite = SKSpriteNode(color: .purple, size: CGSize(width: 100, height: 100))
addChild(gameLayer)
gameLayer.addChild(sprite)
sprite.run(loop)
}
func pause(){
gameLayer.speed = 0
}
override func willMove(from view: SKView) {
super.willMove(from: view)
NotificationCenter.default.removeObserver(self,
name: .UIApplicationDidBecomeActive,
object: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
gameLayer.speed = gameLayer.speed == 0 ? 1 : 0
}
}
I haven't had time to test more, but I was expecting that something like this:
func pause(){
gameLayer.isPaused = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
gameLayer.isPaused = !gameLayer.isPaused
}
would work, but surprisingly it isn't (the node continued with moving after the pause() method was executed). I don't really know if this is a bug or a feature :) but IMHO, I would say it is a bug. Anyways, you can achieve what you want using the speed property. Just copy and paste this code to see how it works.
I've made a maybe not that beautiful, but a very easy workaround.
Just added var gamePaused = false to the Scene class, which is set to true in pause(), and then in
override func update(_ currentTime: TimeInterval) {
guard gamePaused == false else {
if playLayer.isPaused == false {playLayer.isPaused = true}
return
}
.....
}
So each time the app start from the background, the playLayer.isPaused is set to true just with the first rendered frame.

How to make buttons and labels appear and disappear

What I want to know:
I want to know that how to make buttons/labels appear and disappear. When my character collides with an object the buttons/labels will show up over the view and the game-view wont be working any more, only the buttons/labels that appeared can be interacted with.
What I have tried:
I have tried .hidden = false and .hidden = true but it didn't work but maybe I was not using it correctly.
CODE: I have delete unnecessary code!
import Foundation
import AVFoundation
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var movingGround: PPMovingGround!
var square1: PPSquare1!
var square2: PPSquare2!
var wallGen: PPWallGen!
var isStarted = false
var isGameOver = false
override func didMoveToView(view: SKView) {
addMovingGround()
addSquare1()
addWallGen()
start()
}
func addSquare1() {
square1 = PPSquare1()
square1.position = CGPointMake(70, movingGround.position.y + movingGround.frame.size.height/2 + square1.frame.size.height/2)
square1.zPosition = 1
playerNode.addChild(square1)
}
func addWallGen() {
wallGen = PPWallGen(color: UIColor.clearColor(), size: view!.frame.size)
wallGen.position = view!.center
addChild(wallGen)
}
func start() {
isStarted = true
//square2.stop()
square1.stop()
movingGround.start()
wallGen.startGenWallsEvery(1)
}
// MARK - Game Lifecycle
func gameOver() {
isGameOver = true
// everything stops
//square2.fall()
square1.fall()
wallGen.stopWalls()
diamondGen.stopDiamonds()
movingGround.stop()
square1.stop()
//square2.stop()
// create game over label
let gameOverLabel = SKLabelNode(text: "Game Over!")
gameOverLabel.fontColor = UIColor.whiteColor()
gameOverLabel.fontName = "Helvetica"
gameOverLabel.position.x = view!.center.x
gameOverLabel.position.y = view!.center.y + 80
gameOverLabel.fontSize = 22.0
addChild(gameOverLabel)
func restart() {
let newScence = GameScene(size: view!.bounds.size)
newScence.scaleMode = .AspectFill
view!.presentScene(newScence)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
if isGameOver {
restart()
} else {
square1.flip()
}
}
override func update(currentTime: CFTimeInterval) {
// MARK: - SKPhysicsContactDelegate
func didBeginContact(contact: SKPhysicsContact) {
if !isGameOver {
gameOver()
} else {
println("error, not game over!"
}
Without seeing your code, this is a little hard to determine, but I would suggest the following:
Be sure you have connected the buttons to an Outlet variable. This is critical. Without connecting them, you can use the hidden boolean, but it would not have an effect on an actual button.
Be sure you are not somehow undoing your own changes. For example, further down in the code, you might have something which is setting hidden to false even after you set it to true, and so on.
In some cases, you might want to set your outlet variable as strong instead of weak. This may retain changes that are being lost with a view switch.
You can also use "alpha" such as:
myButton.alpha = 0
as an alternate way of controlling visibility. 0 would set the alpha to none (which would make the button invisible) and 1 would set the alpha to full (which would make the button visible again.)
Right after you set hidden (or alpha) put in:
println("i hid the button!")
just to be sure the code you think you are executing really is being executed. Sometimes code we think is not working is actually not even being called.
Please provide more info and I will gladly work to get this solved for you.

Resources