Changing UIImageView transform scale broke movement system - ios

I am trying to create an Image view which I can move and scale on screen. the problem is that when I change the scale of the Image, the movement system seams to be broken.
I wrote some code to drag the object from an anchor point which could be different from the center of the UIImage, but the scale ruined the process.
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
Main view controller for the AR experience.
*/
import ARKit
import SceneKit
import UIKit
import ModelIO
class ViewController: UIViewController, ARSessionDelegate , UIGestureRecognizerDelegate{
// MARK: Outlets
#IBOutlet var sceneView: ARSCNView!
#IBOutlet weak var blurView: UIVisualEffectView!
#IBOutlet weak var dropdown: UIPickerView!
#IBOutlet weak var AddStickerButton: UIButton!
#IBOutlet weak var deleteStickerButton: UIImageView!
var offset : CGPoint = CGPoint.zero
var isDeleteVisible : Bool = false
let array:[String] = ["HappyHeart_Lisa", "Logo_bucato", "Sweety_2_Lisa", "Sweety_Lisa", "Tonglue_Lisa"]
lazy var statusViewController: StatusViewController = {
return childViewControllers.lazy.flatMap({ $0 as? StatusViewController }).first!
}()
var stickers = [Sticker]()
// MARK: Properties
var myScene : SCNScene!
/// Convenience accessor for the session owned by ARSCNView.
var session: ARSession {
sceneView.session.configuration
//sceneView.scene.background.contents = UIColor.black
return sceneView.session
}
var nodeForContentType = [VirtualContentType: VirtualFaceNode]() //Tiene sotto controllo la selezione(Tipo maschera)
let contentUpdater = VirtualContentUpdater() //Chiama la VirtualContentUpdater.swift
var selectedVirtualContent: VirtualContentType = .faceGeometry {
didSet {
// Set the selected content based on the content type.
contentUpdater.virtualFaceNode = nodeForContentType[selectedVirtualContent]
}
}
// MARK: - View Controller Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = contentUpdater
sceneView.session.delegate = self
sceneView.automaticallyUpdatesLighting = true
createFaceGeometry()
// Set the initial face content, if any.
contentUpdater.virtualFaceNode = nodeForContentType[selectedVirtualContent]
// Hook up status view controller callback(s).
statusViewController.restartExperienceHandler = { [unowned self] in
self.restartExperience()
}
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(scale))
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotate))
pinchGesture.delegate = self
rotationGesture.delegate = self
view.addGestureRecognizer(pinchGesture)
view.addGestureRecognizer(rotationGesture)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
/*
AR experiences typically involve moving the device without
touch input for some time, so prevent auto screen dimming.
*/
UIApplication.shared.isIdleTimerDisabled = true
resetTracking()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
session.pause()
}
// MARK: - Setup
/// - Tag: CreateARSCNFaceGeometry
func createFaceGeometry() {
// This relies on the earlier check of `ARFaceTrackingConfiguration.isSupported`.
let device = sceneView.device!
let maskGeometry = ARSCNFaceGeometry(device: device)!
let glassesGeometry = ARSCNFaceGeometry(device: device)!
nodeForContentType = [
.faceGeometry: Mask(geometry: maskGeometry),
.overlayModel: GlassesOverlay(geometry: glassesGeometry),
.blendShapeModel: RobotHead(),
.sfere: RobotHead()
]
}
// MARK: - ARSessionDelegate
func session(_ session: ARSession, didFailWithError error: Error) {
guard error is ARError else { return }
let errorWithInfo = error as NSError
let messages = [
errorWithInfo.localizedDescription,
errorWithInfo.localizedFailureReason,
errorWithInfo.localizedRecoverySuggestion
]
let errorMessage = messages.flatMap({ $0 }).joined(separator: "\n")
DispatchQueue.main.async {
self.displayErrorMessage(title: "The AR session failed.", message: errorMessage)
}
}
func sessionWasInterrupted(_ session: ARSession) {
blurView.isHidden = false
statusViewController.showMessage("""
SESSION INTERRUPTED
The session will be reset after the interruption has ended.
""", autoHide: false)
}
func sessionInterruptionEnded(_ session: ARSession) {
blurView.isHidden = true
DispatchQueue.main.async {
self.resetTracking()
}
}
/// - Tag: ARFaceTrackingSetup
func resetTracking() {
statusViewController.showMessage("STARTING A NEW SESSION")
guard ARFaceTrackingConfiguration.isSupported else { return }
let configuration = ARFaceTrackingConfiguration()
configuration.isLightEstimationEnabled = true
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
// MARK: - Interface Actions
/// - Tag: restartExperience
func restartExperience() {
// Disable Restart button for a while in order to give the session enough time to restart.
statusViewController.isRestartExperienceButtonEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.statusViewController.isRestartExperienceButtonEnabled = true
}
resetTracking()
}
// MARK: - Error handling
func displayErrorMessage(title: String, message: String) {
// Blur the background.
blurView.isHidden = false
// Present an alert informing about the error that has occurred.
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Restart Session", style: .default) { _ in
alertController.dismiss(animated: true, completion: nil)
self.blurView.isHidden = true
self.resetTracking()
}
alertController.addAction(restartAction)
present(alertController, animated: true, completion: nil)
}
//Create a new Sticker
func createNewSticker(){
stickers.append(Sticker(view : self.view, viewCtrl : self))
}
#IBAction func addNewSticker(_ sender: Any) {
createNewSticker()
}
//Function To Move the Stickers, all the Touch Events Listener
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in (touches as! Set<UITouch>) {
var location = touch.location(in: self.view)
for sticker in stickers {
if(sticker.imageView.frame.contains(location) && !isSomeOneMoving()){
//sticker.imageView.center = location
offset = touch.location(in: sticker.imageView)
let offsetPercentage = CGPoint(x: offset.x / sticker.imageView.bounds.width, y: offset.y / sticker.imageView.bounds.height)
let offsetScaled = CGPoint(x: sticker.imageView.frame.width * offsetPercentage.x, y: sticker.imageView.frame.height * offsetPercentage.y)
offset.x = (sticker.imageView.frame.width / 2) - offsetScaled.x
offset.y = (sticker.imageView.frame.height / 2) - offsetScaled.y
location = touch.location(in: self.view)
location.x = (location.x + offset.x)
location.y = (location.y + offset.y)
sticker.imageView.center = location
disableAllStickersMovements()
isDeleteVisible = true
sticker.isStickerMoving = true;
deleteStickerButton.isHidden = false
}
}
}
}
func disableAllStickersMovements(){
for sticker in stickers {
sticker.isStickerMoving = false;
}
}
func isSomeOneMoving() -> Bool{
for sticker in stickers {
if(sticker.isStickerMoving){
return true
}
}
return false
}
var lastLocationTouched : CGPoint = CGPoint.zero
var lastStickerTouched : Sticker = Sticker()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in (touches as! Set<UITouch>) {
var location = touch.location(in: self.view)
for sticker in stickers {
if(sticker.imageView.frame.contains(location) && sticker.isStickerMoving){
lastLocationTouched = location
location = touch.location(in: self.view)
location.x = (location.x + offset.x)
location.y = (location.y + offset.y)
sticker.imageView.center = location
//sticker.imageView.center = location
}
if(deleteStickerButton.frame.contains(lastLocationTouched) && isDeleteVisible && sticker.isStickerMoving){
sticker.imageView.alpha = CGFloat(0.5)
}else{
sticker.imageView.alpha = CGFloat(1)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for sticker in stickers {
if(deleteStickerButton.frame.contains(lastLocationTouched) && isDeleteVisible && sticker.isStickerMoving){
removeASticker(sticker : sticker)
disableAllStickersMovements()
}
}
disableAllStickersMovements()
isDeleteVisible = false
deleteStickerButton.isHidden = true
}
func removeASticker(sticker : Sticker ){
sticker.imageView.removeFromSuperview()
let stickerPosition = stickers.index(of: sticker)!
stickers.remove(at: stickerPosition)
for sticker in stickers {
sticker.isStickerMoving = false;
}
}
var identity = CGAffineTransform.identity
#objc func scale(_ gesture: UIPinchGestureRecognizer) {
for sticker in stickers {
if(sticker.isStickerMoving){
switch gesture.state {
case .began:
identity = sticker.imageView.transform
case .changed,.ended:
sticker.imageView.transform = identity.scaledBy(x: gesture.scale, y: gesture.scale)
case .cancelled:
break
default:
break
}
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#objc func rotate(_ gesture: UIRotationGestureRecognizer) {
for sticker in stickers {
if(sticker.isStickerMoving){
sticker.imageView.transform = sticker.imageView.transform.rotated(by: gesture.rotation)
}
}
}
}
and then the sticker class
import UIKit
import Foundation
class Sticker : NSObject, UIGestureRecognizerDelegate{
var location = CGPoint(x: 0 , y: 0);
var sticker_isMoving = false;
let imageView = UIImageView()
var isStickerMoving : Bool = false;
init(view : UIView, viewCtrl : ViewController ) {
super.init()
imageView.image = UIImage(named: "BroccolFace_Lisa.png")
imageView.isUserInteractionEnabled = true
imageView.contentMode = UIViewContentMode.scaleAspectFit
imageView.frame = CGRect(x: view.center.x, y: view.center.y, width: 200, height: 200)
view.addSubview(imageView)
}
override init(){
}
}

This is because the imageView.bounds and the touch.location(in: imageView) are in unscaled values. This will overcome the problem:
offset = touch.location(in: imageView)
let offsetPercentage = CGPoint(x: offset.x / imageView.bounds.width, y: offset.y / imageView.bounds.height)
let offsetScaled = CGPoint(x: imageView.frame.width * offsetPercentage.x, y: imageView.frame.height * offsetPercentage.y)
offset.x = (imageView.frame.width / 2) - offsetScaled.x
offset.y = (imageView.frame.height / 2) - offsetScaled.y
Basically it converts the offset into a percentage based on the unscaled values and then converts that into scaled values based on the imageView frame (which is modified by the scale). It then uses that to calculate the offset.
EDIT (NUMBER TWO)
This is more complete way to do it and it should solve any issues that may arise due to scaling or rotation.
Add this structure to hold the details of the dragging for images:
struct DragInfo {
let imageView: UIImageView
let startPoint: CGPoint
}
Add these instance variables (you can also remove offset if you want):
var dragStartPoint: CGPoint = CGPoint.zero
var currentDragItems: [DragInfo] = []
var dragTouch: UITouch?
Change touchesBegan to this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard self.dragTouch == nil, let touch = touches.first else { return }
self.dragTouch = touch
let location = touch.location(in: self.view)
self.dragStartPoint = location
for imageView in self.imageList {
if imageView.frame.contains(location) {
self.currentDragItems.append(DragInfo(imageView: imageView, startPoint: imageView.center))
}
}
}
Change touchesMoved to this:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let dragTouch = self.dragTouch else { return }
for touch in touches {
if touch == dragTouch {
let location = touch.location(in: self.view)
let offset = CGPoint(x: location.x - self.dragStartPoint.x, y: location.y - self.dragStartPoint.y)
for dragInfo in self.currentDragItems {
let imageOffSet = CGPoint(x: dragInfo.startPoint.x + offset.x, y: dragInfo.startPoint.y + offset.y)
dragInfo.imageView.center = imageOffSet
}
}
}
}
Change touchesEnded to this:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let dragTouch = self.dragTouch, touches.contains(dragTouch) else { return }
self.currentDragItems.removeAll()
self.dragTouch = nil
}
Set the following properties on the gesture recognisers used:
scaleGesture.delaysTouchesEnded = false
scaleGesture.cancelsTouchesInView = false
rotationGesture.delaysTouchesEnded = false
rotationGesture.cancelsTouchesInView = false
Some explanation about how it works.
With all the touch events it only considers the first touch because dragging from multiple touches doesn't make much sense (what if two touches were over the same image view and move differently). It records this touch and then only considers that touch for dragging things around.
When touchesBegan is called it checks no touch for dragging exists (indicating a drag in progress) and it finds all image views that are under the touch and for each one it records the details of itself and it start centre position in a DragInfo structure and stores it in the currentDragItems array. It also records the position the touch started in the main view and the touch that initiated it.
When touchesMoved is called it only considers the touch that started the dragging and it calculates the offset from the original position the touch started in the main view and then goes down the list of images involved in the dragging and calculates their new centre based on their original starting position and the offset calculated and sets that as the new centre.
When touchesEnded is called assuming it is the dragging touch that is ended it clears the array of DragInfo objects to ready for the next drag.
You need to set the delaysTouchesEnded and cancelsTouchesInView properties on all gesture recognisers so that all touches are passed through to the view otherwise the touchesEnded methods in particular are not called.
Doing the calculations like this removes the problems of scale and rotation as you are just concerned with offsets from initial positions. It also works if multiple image views are dragged at the same time as their details are kept separately.
Now there are some things to be aware of:
You will need to put in all the other code you app required as this is just a basic example to show the idea.
This assumes that you only want to drag image views that you pick up at the start. If you want to collect image views as you drag around you would need to develop a much more complicated system.
As I stated only one drag operation can be in progress at a time and it takes the first touch registered as this source touch. This source touch is then used to filter out any other touches that may happen. This is done to keep things simple and otherwise you would have to account for all kinds of strange situations like if multiple touches were on the same image view.
I hope this all makes sense and you can adapt it to solve your problem.

Here is an extension that I use to pan, pinch and rotate an image with UIPanGestureRecognizer, UIPinchGestureRecognizer and UIRotationGestureRecognizer
extension ViewController : UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func panGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .ended: fallthrough
case .changed:
let translation = gesture.translation(in: gesture.view)
if let view = gesture.view {
var finalPoint = CGPoint(x:view.center.x + translation.x, y:view.center.y + translation.y)
finalPoint.x = min(max(finalPoint.x, 0), self.myImageView.bounds.size.width)
finalPoint.y = min(max(finalPoint.y, 0), self.myImageView.bounds.size.height)
view.center = finalPoint
gesture.setTranslation(CGPoint.zero, in: gesture.view)
}
default : break
}
}
func pinchGesture(gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .changed:
let scale = gesture.scale
gesture.view?.transform = gesture.view!.transform.scaledBy(x: scale, y: scale)
gesture.scale = 1
default : break
}
}
func rotateGesture(gesture: UIRotationGestureRecognizer) {
switch gesture.state {
case .changed:
let rotation = gesture.rotation
gesture.view?.transform = gesture.view!.transform.rotated(by: rotation)
gesture.rotation = 0
default : break
}
}
}
setting the UIGestureRecognizerDelegate will help you do the three of gestures at the same time.

Related

How to change camera position by dragging finger on the screen - Swift

I'm trying to learn ARKIT and make a small demo app to draw in 3D.
The following is the code I wrote and so far there are no problems:
import UIKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet weak var sceneView: ARSCNView!
#IBOutlet weak var DRAW: UIButton!
#IBOutlet weak var DEL: UIButton!
let config = ARWorldTrackingConfiguration()
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.session.run(config)
self.sceneView.delegate = self
}
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else {return}
let transform = pointOfView.transform
let cameraOrientation = SCNVector3(-transform.m31,-transform.m32,-transform.m33)
let cameraLocation = SCNVector3(transform.m41,transform.m42,transform.m43)
let cameraCurrentPosition = cameraOrientation + cameraLocation
DispatchQueue.main.async {
if (self.DRAW.isTouchInside){
let sphereNode = SCNNode(geometry: SCNSphere(radius: 0.02))
sphereNode.position = cameraCurrentPosition
self.sceneView.scene.rootNode.addChildNode(sphereNode)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
print("RED Button is Pressed")
}else if (self.DEL.isTouchInside){
self.sceneView.scene.rootNode.enumerateChildNodes{
(node, stop) in
node.removeFromParentNode()
}
}else{
let pointer = SCNNode(geometry: SCNSphere(radius: 0.01))
pointer.name = "pointer"
pointer.position = cameraCurrentPosition
self.sceneView.scene.rootNode.enumerateChildNodes({(node,_) in
if node.name == "pointer"{
node.removeFromParentNode()
}
})
self.sceneView.scene.rootNode.addChildNode(pointer)
pointer.geometry?.firstMaterial?.diffuse.contents = UIColor.purple
}
}
}
}
func +(left:SCNVector3,right:SCNVector3) -> SCNVector3 {
return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z)
}
As you can see, I set the scene and configure it,
I create a button to draw when pressed, a pointer (or viewfinder) that takes the center of the scene and a button to delete the nodes inserted.
Now I would like to be able to move the cameraCurrentPosition to a different point from the center: I would like to move it if possible with a touch on the screen taking the position of the finger.
If possible, could someone help me with the code?
Generally speaking, you can't programmatically move the Camera within an ARSCN, the camera transform is the physical position of the device relative to the virtual scene.
With that being said, one way you could draw the user touches to the screen is using the touchesMoved method within your View Controller.
var touchRoots: [SCNNode] = [] // list of root nodes for each set of touches drawn
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// get the initial touch event
if let touch = touches.first {
guard let pointOfView = self.sceneView.pointOfView else { return }
let transform = pointOfView.transform // transformation matrix
let orientation = SCNVector3(-transform.m31, -transform.m32, -transform.m33) // camera rotation
let location = SCNVector3(transform.m41, transform.m42, transform.m43) // location of camera frustum
let currentPostionOfCamera = orientation + location // center of frustum in world space
DispatchQueue.main.async {
let touchRootNode : SCNNode = SCNNode() // create an empty node to serve as our root for the incoming points
touchRootNode.position = currentPostionOfCamera // place the root node ad the center of the camera's frustum
touchRootNode.scale = SCNVector3(1.25, 1.25, 1.25)// touches projected in Z will appear smaller than expected - increase scale of root node to compensate
guard let sceneView = self.sceneView else { return }
sceneView.scene.rootNode.addChildNode(touchRootNode) // add the root node to the scene
let constraint = SCNLookAtConstraint(target: self.sceneView.pointOfView) // force root node to always face the camera
constraint.isGimbalLockEnabled = true // enable gimbal locking to avoid issues with rotations from LookAtConstraint
touchRootNode.constraints = [constraint] // apply LookAtConstraint
self.touchRoots.append(touchRootNode)
}
}
}
override func func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let translation = touch.location(in: self.view)
let translationFromCenter = CGPoint(x: translation.x - (0.5 * self.view.frame.width), y: translation.y - (0.5 * self.view.frame.height))
// add nodes using the main thread
DispatchQueue.main.async {
guard let touchRootNode = self.touchRoots.last else { return }
let sphereNode : SCNNode = SCNNode(geometry: SCNSphere(radius: 0.015))
sphereNode.position = SCNVector3(-1*Float(translationFromCenter.x/1000), -1*Float(translationFromCenter.y/1000), 0)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.white
touchRootNode.addChildNode(sphereNode) // add point to the active root
}
}
}
Note: solution only handles a single touch, but it is simple enough to extend the example to add multi-touch support.

Custom UIButton .touchDragEnter and .touchDragExit area/size?

Is it possible to customize the area from the button at which it is considered .touchDragExit (or .touchDragEnter) (out of its selectable area?)?
To be more specific, I am speaking about this situation: I tap the UIButton, the .touchDown gets called, then I start dragging my finger away from the button and at some point (some distance away) it will not select anymore (and of course I can drag back in to select...). I would like the modify that distance...
Is this even possible?
You need to overwrite the UIButton continueTracking and touchesEnded functions.
Adapting #Dean's link, the implementation would be as following (swift 4.2):
class ViewController: UIViewController {
#IBOutlet weak var button: DragButton!
override func viewDidLoad() {
super.viewDidLoad()
}
}
class DragButton: UIButton {
private let _boundsExtension: CGFloat = 0 // Adjust this as needed
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let previousLocation: CGPoint = touch.previousLocation(in: self)
let touchOutside: Bool = !outerBounds.contains(currentLocation)
if touchOutside {
let previousTouchInside: Bool = outerBounds.contains(previousLocation)
if previousTouchInside {
print("touchDragExit")
sendActions(for: .touchDragExit)
} else {
print("touchDragOutside")
sendActions(for: .touchDragOutside)
}
} else {
let previousTouchOutside: Bool = !outerBounds.contains(previousLocation)
if previousTouchOutside {
print("touchDragEnter")
sendActions(for: .touchDragEnter)
} else {
print("touchDragInside")
sendActions(for: .touchDragInside)
}
}
return true
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let touchInside: Bool = outerBounds.contains(currentLocation)
if touchInside {
print("touchUpInside action")
return sendActions(for: .touchUpInside)
} else {
print("touchUpOutside action")
return sendActions(for: .touchUpOutside)
}
}
}
Try changing the _boundsExtension value
The drag area is exaclty equal to the area define by bounds.
So if you want to customize the drag are simple customise the bounds of your button.

Draw app is Lagging when adding shapeLayer into views layer in Swift / IOS

I am creating a drawing app. This app allows user to draw on the view. i name that view drawing view in the code given below. To create this app I have use BeizerPath and ShapeLayer, whenever user touches the screen the UIBeizerPath and CAShapeLayer is initialized. The Problem with this app is after some drawing view gets Lagged. I cannot figure out what is happening. Is there better way to optimize this app?
CanvasViewController: UIViewController {
// MARK: Properties
#IBOutlet weak var verticalScrollView: UIScrollView!
var lastPoint:CGPoint? // var mutablePath:CGMutablePath?
var drawLine:UIBezierPath?
var shapeLayer:CAShapeLayer?
let dropDown = DropDown()
var shape:CAShapeLayer?
let listOfDropDownMenu:[String] = ["Create New Canvas","Save","Change Color"]
var presenter: CanvasModuleInterface?
// MARK: IBOutlets
#IBOutlet weak var drawingView: UIView!
#IBOutlet weak var showColorViewControllerDropDownButton: UIBarButtonItem!
// MARK: VC's Life cycle
override func viewDidLoad() {
super.viewDidLoad()
self.setup()
self.verticalScrollView.isUserInteractionEnabled = false
}
// MARK: IBActions
// MARK: Other Functions
#IBAction func ShowColorViewControllerAction(_ sender: Any) {
self.dropDown.anchorView = self.showColorViewControllerDropDownButton
self.dropDown.dataSource = self.listOfDropDownMenu
self.dropDown.selectionAction = { [unowned self] (index: Int, item: String) in
switch index{
case 0:
self.createNewView()
case 1:
self.saveTheCanvas()
case 2:
self.presentToColorViewController()
//self.presentColorController()
default:
print("out of order !!!")
}
}
self.dropDown.show()
}
private func createNewView(){
//Mark: - create new canvas
self.drawingView.layer.sublayers!.forEach { (layers) in
layers.removeFromSuperlayer()
}
}
private func presentToColorViewController(){
// Mark : - naviagate to color view controller.
}
private func saveTheCanvas(){
// Mark: - save the drawing
}
private func setup() {
// all setup should be done here
}
private func presentColorController(){
let colorSelectionController = EFColorSelectionViewController()
let colorNavigationController = UINavigationController(rootViewController: colorSelectionController)
colorNavigationController.navigationBar.backgroundColor = UIColor.white
colorNavigationController.navigationBar.isTranslucent = false
colorNavigationController.modalPresentationStyle = UIModalPresentationStyle.popover
colorSelectionController.delegate = self
colorSelectionController.color = self.view.backgroundColor ?? UIColor.white
if UIUserInterfaceSizeClass.compact == self.traitCollection.horizontalSizeClass {
let doneButton: UIBarButtonItem = UIBarButtonItem(
title: NSLocalizedString("Done", comment: ""),
style: UIBarButtonItem.Style.done,
target: self,
action: #selector(dismissViewController)
)
colorSelectionController.navigationItem.rightBarButtonItem = doneButton
}
self.present(colorNavigationController, animated: true, completion: nil)
}
#objc func dismissViewController(){
//##ask confusion.....
self.dismiss(animated: true, completion: nil)
}
}
// MARK: CanvasViewInterface extension CanvasViewController: CanvasViewInterface {
} extension CanvasViewController: EFColorSelectionViewControllerDelegate{
func colorViewController(_ colorViewCntroller: EFColorSelectionViewController, didChangeColor color: UIColor) {
} } extension CanvasViewController{
//Mark: - this extension contains function for drawing the circle.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touches begin")
self.drawLine = UIBezierPath()
self.shapeLayer = CAShapeLayer()
// self.mutablePath = CGMutablePath()
if let touchesPoint = touches.first{
self.lastPoint = touchesPoint.location(in: self.drawingView)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// self.drawingView.layer.sublayers!.forEach { (layer) in // print(layer) // }
// print(self.drawingView.layer)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?){ // print("\(self.drawingView)") // print("\(self.drawingView.layer.sublayers)")
var nextPoint:CGPoint?
if let touchesPoint = touches.first{
nextPoint = touchesPoint.location(in: self.drawingView)
guard let lastLinePoint = self.lastPoint , let nextLinePoint = nextPoint else{return}
self.drawLineInDrawingView(from: lastLinePoint, to: nextLinePoint)
}
if let newPoint = nextPoint{
self.lastPoint = newPoint
}
}
func drawLineInDrawingView(from:CGPoint,to:CGPoint){
drawLine!.move(to: CGPoint(x: from.x, y: from.y))
drawLine!.addLine(to: CGPoint(x: to.x, y: to.y))
shapeLayer!.path = drawLine!.cgPath
shapeLayer!.strokeColor = UIColor.black.cgColor
shapeLayer!.lineCap = .round
shapeLayer!.lineWidth = 100
self.drawingView.layer.addSublayer(shapeLayer!) // print(self.drawingView.layer.sublayers)
}
}
Without knowing what CanvasViewController you are are using, one can't entirely answer your question correctly, but I will answer, assuming it works similar to any regular UIView in iOS.
There are a few things I can point out to improve performance:
First of all, don't add the same ShapeLayer over and over inside drawLineInDrawingView(from:CGPoint,to:CGPoint) to the drawing view, instead do so only once in touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) and store a reference to it.
Secondly your drawLineInDrawingView(from:CGPoint,to:CGPoint) should look like this:
func drawLineInDrawingView(from:CGPoint,to:CGPoint){
CATransaction.begin()
CATransaction.setDisableActions(true)
drawLine!.move(to: CGPoint(x: from.x, y: from.y))
drawLine!.addLine(to: CGPoint(x: to.x, y: to.y))
shapeLayer!.path = drawLine!.cgPath
shapeLayer!.strokeColor = UIColor.black.cgColor
shapeLayer!.lineCap = .round
shapeLayer!.lineWidth = 100
shapeLayer!.setNeedsDisplay()
CATransaction.commit()
}
So in conclusion: Upon touchesBegan() you add the shapeLayer and store a reference to it. Inside touchesMoved, you call the drawLineInDrawingsFunction.
Your drawLineInDrawingsFunction, only updates your Shapelayer with shapeLayer.setNeedsDisplay().
Putting your transformation inside
CATransaction.begin()
CATransaction.setDisableActions(true)
/**Your transformation here**/
CATransaction.commit()
Is only there to stop your View from automatically animating changes, and instead show them immediately.
I hope this helps.

Scrollview stopping at a certain point

Hi I currently have a horizontal UIScrollView that allows me to pick a character in my game and then select it but the problem that I have now is I am trying to get the scrollview to stop at the point where the sprite/character is in the middle of the screen and instead of having it stop anywhere.
My UIScrollView has a “moveable node” that contains multiple pages that hold all the sprites as seen below:
scrollViewHorizontal = CustomScrollView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height), scene: self, moveableNode: moveableNodeHorizontal, scrollDirection: .Horizontal)
scrollViewHorizontal.contentSize = CGSizeMake(self.frame.size.width * 4, self.frame.size.height) // * 4 makes it three times as wide as screen
view?.addSubview(scrollViewHorizontal)
scrollViewHorizontal.hidden = true
addChild(moveableNodeHorizontal)
moveableNodeHorizontal.hidden = true
moveableNodeHorizontal.zPosition = 100000
scrollViewHorizontal.setContentOffset(CGPoint(x: 0 + self.frame.size.width + self.frame.size.width * 2, y: 0), animated: false)
let page1ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(scrollViewHorizontal.frame.size.width, scrollViewHorizontal.frame.size.height))
page1ScrollView.zPosition = -1
page1ScrollView.position = CGPointMake(CGRectGetMidX(self.frame) - (self.frame.size.width * 3.5), CGRectGetMidY(self.frame) - (self.frame.height / 2))
moveableNodeHorizontal.addChild(page1ScrollView)
let page2ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(scrollViewHorizontal.frame.size.width, scrollViewHorizontal.frame.size.height))
page2ScrollView.zPosition = -1
page2ScrollView.position = CGPointMake(CGRectGetMidX(self.frame) - (self.frame.size.width * 2.5), CGRectGetMidY(self.frame) - (self.frame.height / 2))
moveableNodeHorizontal.addChild(page2ScrollView)
Characters.append(generateCharacters(CGPointMake(0, 0), page:(page1ScrollView), tex: "YellowFrog"))
Characters.append(generateCharacters(CGPointMake(100, 0), page:(page1ScrollView), tex: "Frog"))
Characters.append(generateCharacters(CGPointMake(0, 0), page:(page2ScrollView), tex: "ball"))
Characters.append(generateCharacters(CGPointMake(100, 0), page:(page2ScrollView), tex: "ball"))
I searched this previously before asking the question and it was recommended that I use this:
func scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
But I have no idea how to implement it into my code because my scrollview is contained in another class which can be seen here:
/// Nodes touched
var nodesTouched: [AnyObject] = [] // global
/// Scroll direction
enum ScrollDirection: Int {
case None = 0
case Vertical
case Horizontal
}
/// Custom UIScrollView class
class CustomScrollView: UIScrollView {
// MARK: - Static Properties
/// Touches allowed
static var disabledTouches = false
/// Scroll view
private static var scrollView: UIScrollView!
// MARK: - Properties
/// Nodes touched. This will forward touches to node subclasses.
private var nodesTouched = [AnyObject]()
/// Current scene
private let currentScene: SKScene
/// Moveable node
private let moveableNode: SKNode
/// Scroll direction
private let scrollDirection: ScrollDirection
// MARK: - Init
init(frame: CGRect, scene: SKScene, moveableNode: SKNode, scrollDirection: ScrollDirection) {
self.currentScene = scene
self.moveableNode = moveableNode
self.scrollDirection = scrollDirection
super.init(frame: frame)
CustomScrollView.scrollView = self
self.frame = frame
delegate = self
indicatorStyle = .White
scrollEnabled = true
userInteractionEnabled = true
//canCancelContentTouches = false
//self.minimumZoomScale = 1
//self.maximumZoomScale = 3
if scrollDirection == .Horizontal {
let flip = CGAffineTransformMakeScale(-1,-1)
transform = flip
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Touches
extension CustomScrollView {
/// Began
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//super.touchesBegan(touches, withEvent: event)
for touch in touches {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches began in current scene
currentScene.touchesBegan(touches, withEvent: event)
/// Call touches began in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesBegan(touches, withEvent: event)
}
}
}
/// Moved
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
//super.touchesMoved(touches, withEvent: event)
for touch in touches {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches moved in current scene
currentScene.touchesMoved(touches, withEvent: event)
/// Call touches moved in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesMoved(touches, withEvent: event)
}
}
}
/// Ended
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
//super.touchesEnded(touches, withEvent: event)
for touch in touches {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches ended in current scene
currentScene.touchesEnded(touches, withEvent: event)
/// Call touches ended in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesEnded(touches, withEvent: event)
}
}
}
/// Cancelled
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
//super.touchesCancelled(touches, withEvent: event)
for touch in touches! {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches cancelled in current scene
currentScene.touchesCancelled(touches, withEvent: event)
/// Call touches cancelled in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesCancelled(touches, withEvent: event)
}
}
}
}
// MARK: - Touch Controls
extension CustomScrollView {
/// Disable
class func disable() {
CustomScrollView.scrollView?.userInteractionEnabled = false
CustomScrollView.disabledTouches = true
}
/// Enable
class func enable() {
CustomScrollView.scrollView?.userInteractionEnabled = true
CustomScrollView.disabledTouches = false
}
}
// MARK: - Delegates
extension CustomScrollView: UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollDirection == .Horizontal {
moveableNode.position.x = scrollView.contentOffset.x
} else {
moveableNode.position.y = scrollView.contentOffset.y
}
}
}
EDIT:
extension CustomScrollView: UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollDirection == .Horizontal {
moveableNode.position.x = scrollView.contentOffset.x
} else {
moveableNode.position.y = scrollView.contentOffset.y
}
}
}
CustomScrollView is a project showed how UIKit could be used in sprite-kit but this causes a lot of work during rendering that slows your game (due to low fps) so it's advisable and reasonable to use it just only for your game menus, not for the game.
A way to iOS8.x:
You can build for example many SKSpriteNode backgrounds that follow the one with the other and in your update method doing:
override func update(currentTime: CFTimeInterval) {
self.enumerateChildNodesWithName("background") { node, _ in
if node is SKSpriteNode {
if let p = (node as! SKShapeNode).position {
// adjust your background positions to make the scroll
}
}
}
}
A way to iOS9x:
Apple added the SKCameraNode class to sprite-kit in iOS 9 to make it easier to scroll and zoom in sprite-kit scenes.
var gameCamera = SKCameraNode()
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
let previousLocation = touch.previousLocationInNode(self)
let deltaX = location.x - previousLocation.x
gameCamera.position.x += deltaX
}
}
The CustomScrollView page selector.
Having said that we come to your situation, suppose you have all your pages in array called pages and you want to stop the scroll to the x page:
func scrollToPage(page:Int) {
let totalPages : Int = (pages.count - 1)
let reversedPage : Int = totalPages - page
self.scrollView.setContentOffset(CGPointMake(round(self.frame.size.width*CGFloat(reversedPage)), scrollView.contentOffset.y),animated: true)
}
Another good idea it's to intercept the scroll delegate methods to update your elements:
func scrollViewDidScroll(scrollView: UIScrollView) {
self.scrollView.scrollViewDidScroll(scrollView)
updateLayout() // call my method to update elements in scroll checking the current page
}
You could obtain your page number whit this code:
extension UIScrollView {
var currentPage: Int {
return abs(Int(round((contentSize.width - self.contentOffset.x) / self.bounds.size.width))-1)
}
}

Add UIView to back of scene in SpriteKit game

I am trying to make a advent calendar with a snow affect in swift2. I am using the game template while using SpiteKit.
Here is my code so far:
GameScene.swift
import SpriteKit
class GameScene: SKScene {
/*override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
print(location)
}
}*/
func test()
{
//Generate Doors
//Initilization
var adventDoors = [AdventDoor]()
let offset = CGVector(dx: 10,dy: 10)
var size = CGRectMake(offset.dx, offset.dy, 60, 60)
var ypos:CGFloat = 20
var xpos:CGFloat = 10
var index = 0
var xDoor = AdventDoor(frame: size)
let randomIdentifier = [Int](1...24).shuffle()
for _ in 1...4
{
for i in 1...6
{
size = CGRectMake(xpos, ypos, 60, 60)
xDoor = AdventDoor(frame: size)
xDoor.opaque = false
xDoor.restorationIdentifier = "\(randomIdentifier[index])"
xDoor.generateDoor()
adventDoors.append(xDoor)
print("1...6")
ypos += 80
//xpos += 20
index++
if i == 6
{
print("Moving to next view")
}
}
xpos += 80
ypos = 20
}
size = CGRectMake(10, 500, 300, 60)
xDoor = AdventDoor(frame: size)
xDoor.opaque = false
xDoor.restorationIdentifier = "\(25)"
xDoor.generateDoor()
adventDoors.append(xDoor)
index = 0
for door in adventDoors
{
index++
self.view?.addSubview(door)
}
print("\(index) doors were added")
}
}
GameViewController.swift
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
//skView.showsFPS = true
//skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
scene.backgroundColor = UIColor.greenColor()
skView.presentScene(scene)
scene.test()
//Snow
let wrappedSnowPath = NSBundle.mainBundle().pathForResource("Snow", ofType: "sks")
if let snowPath = wrappedSnowPath
{
let snowEmitter:SKEmitterNode = NSKeyedUnarchiver.unarchiveObjectWithFile(snowPath) as! SKEmitterNode
let screenBounds = UIScreen.mainScreen().bounds
snowEmitter.position = CGPointMake(screenBounds.width, screenBounds.height)
scene.addChild(snowEmitter)
}
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
AdventDoor.swift just contains a custom UIView (AdventDoor) along with some more functions
This is what it looks like.
As you can see, the snow SKEmitterNode particles are behind the AdventDoors and not in front.
How would I get my snow to display in front of the UIView Advent Doors instead of behind?
Instead of addsubview for the door, What you need to do is rearrange how your views are laid out. What you need is a UIView as your main view, then you add the SKView as a child to the main view. Then if you want to add the doors in during the scene creation process, you need to do self.view.superview.insertSubView(door, atIndex:0) or self.view.superview.insertSubView(door, belowSubView:self.view) so that the doors are placed behind the scene subview

Resources