why is my gestureRecognizer returning nil? - swift

I have tried to combine numerous tutorials to try and make a gesture recognizer start an animation on a tableView once a gesture is received on a mapView (shrinking table once map is moved like the old uber app). The animation functions, however once the animation is finished i am receiving "unexpectedly found nil when unwrapping an optional" - however it seems like it should not be nil, obviously it is, i just dont understand how. The error is 3/4 the way down, ive tried to reduce as much code as possible so parts are missing. My guess is that i have created 2 gesture recognizers which is causing the issue, but i am unsure how to combine them. Here is the code:
class RestaurantsVC: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, UIGestureRecognizerDelegate {
var animator: UIViewPropertyAnimator?
var currentState: AnimationState!
var thumbnailFrame: CGRect!
var panGestureRecognizer: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(RestaurantsVC.handlePan(gestureRecognizer:)))
panGestureRecognizer.delegate = self
self.mapView.addGestureRecognizer(panGestureRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#objc func handlePan (gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: self.view.superview)
switch gestureRecognizer.state {
case .began:
startPanning()
animator?.startAnimation()
case .ended:
let velocity = gestureRecognizer.velocity(in: self.view.superview)
endAnimation(translation: translation, velocity: velocity)
default:
print("Something went wrong handlePan")
}
}
func startPanning() {
var finalFrame:CGRect = CGRect()
switch currentState {
// code
}
animator = UIViewPropertyAnimator(duration: 1, dampingRatio: 0.8, animations: {
})
}
func endAnimation (translation:CGPoint, velocity:CGPoint) {
if let animator = self.animator {
self.panGestureRecognizer.isEnabled = false //(this line is where i get the error)//
switch self.currentState {
case .thumbnail:
animator.isReversed = false
animator.addCompletion({ _ in
self.currentState = .minimized
self.panGestureRecognizer.isEnabled = true
})
case .minimized:
animator.isReversed = true
animator.addCompletion({ _ in
self.currentState = .thumbnail
self.panGestureRecognizer.isEnabled = true
})
default:
print("unknown state")
}
}
}
}

It's nil because you're never actually assigning it.
In your viewDidLoad() function, you're actually redefining a local variable which shadows the instance variable:
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(RestaurantsVC.handlePan(gestureRecognizer:)))
^ The 'let' keyword is redefining this variable at the local scope
You should remove the let declaration, and instead assign this as:
self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(RestaurantsVC.handlePan(gestureRecognizer:)))

This could work still without storing a reference to the gesture recognizer. You could pass the values from the gesture recognizer passed into handlePan into startAnimation and endAnimation. This would reduce the need to have superfluous properties on your class

Related

UIView Representative Does Not Conform to Protocol

I'm new to coding and am working on an app to display graphs using XCODE (13.1)/Swift. I want to be able to swipe to go from graph to graph, but have been unable to get it functioning. I originally tried to use a class to call for the swipe, but ran into conflicts with the UIView.
I think that the answer is to use a UIViewRepresentative, but I got an error that it did not conform to protocol. Here is the code for the approach using class (I have commented the options out as I have been troubleshooting.
/*class SwipeView: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
self.view.isUserInteractionEnabled = true
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(tapForSwipe))
swipeRight.direction = .right
self.view.addGestureRecognizer(swipeRight)
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(tapForSwipe))
swipeLeft.direction = .left
self.view.addGestureRecognizer(swipeLeft)
}
#objc private func tapForSwipe(sender : UISwipeGestureRecognizer){
if sender.direction == .right {
Charttype = Charttype - 1
print("right")
}
else if sender.direction == .left {
Charttype = Charttype + 1
return
}
if Charttype > 16 {
Charttype = 16
return
}
if Charttype < 0 {
Charttype = 0
return
}
And here is the code for the UIViewRepresentative approach:
enter
struct swipeGesture : UIViewRepresentable {
func makeCoordinator() -> swipeGesture.Coordinator {
return swipeGesture.Coordinator()
}
func makeUIView(context: UIViewControllerRepresentableContext<swipeGesture>) -> UIView{
let view = UIView()
let left = UISwipeGestureRecognizer(target :context.coordinator, action: #selector(context.coordinator.left))
left.direction = .left
let right = UISwipeGestureRecognizer(target: self, action: #selector(context.coordinator.right))
right.direction = .right
view.addGestureRecognizer(left)
view.addGestureRecognizer(right)
return view
}
func updateUIView(_ uiView: UIView, context: UIViewControllerRepresentableContext<swipeGesture>) {
}
class Coordinator : NSObject{
#objc func left(){
print("left")
}
#objc func right(){
print("right")
}
}
I appreciate any help and guidance on this. Thanks!
You're confusing UIViewControllerRepresentable and UIViewRepresentable. Your two function calls above need to be:
func updateUIView(_ uiView: UIView, context: Self.Context) {
and
func makeUIView(context: Self.Context) -> UIView {
You're using UIViewControllerRepresentableContext<swipeGesture> which is not relevant here, and the source of your problems. The error message you're seeing is that swipeGesture (should be SwipeGesture, by the way, types start with upper case letters) doesn't conform to UIViewControllerRepresentable, which is true. The compiler only thinks it should because you've mentioned it in the signature of these two methods.
The wider issue may be why you think this is a solution to your problem. UIViewRepresentable is for injecting UIKit views into a SwiftUI app, yet from your question it seems like you're building a UIKit app anyway, so I'm not sure why you're going this route. But that's a different question, I think.

Adding a completion handler to UIViewPropertyAnimator in Swift

I'm trying to get a property animator to start animation when a View Controller is presented.
Right now the animation is playing however the UIViewPropertyAnimator doesn't respond to the completion handler added to it.
UIVisualEffectView sub-class.
import UIKit
final class BlurEffectView: UIVisualEffectView {
deinit {
animator?.stopAnimation(true)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
effect = nil
animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in
self.effect = theEffect
}
animator?.pausesOnCompletion = true
}
private let theEffect: UIVisualEffect = UIBlurEffect(style: .regular)
var animator: UIViewPropertyAnimator?
}
First View controller
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func doSomething(_ sender: UIButton) {
let vc = storyboard?.instantiateViewController(identifier: "second") as! SecondVC
vc.modalPresentationStyle = .overFullScreen
present(vc, animated: false) { //vc presentation completion handler
//adding a completion handler to the UIViewPropertyAnimator
vc.blurView.animator?.addCompletion({ (pos) in
print("animation complete") //the problem is that this line isn't executed
})
vc.blurView.animator?.startAnimation()
}
}
}
Second view controller
import UIKit
class SecondVC: UIViewController {
#IBOutlet weak var blurView: BlurEffectView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
Here the UIViewPropertyAnimator completion handler is added after the Second View Controller(controller with visual effect view) is presented. I have tried moving the completion handler to different places like viewDidLoad and viewDidAppear but nothing seems to work.
This whole thing seems incorrectly designed.
draw(_ rect:) is not the place to initialize your animator*, my best guess at what's happening is that vc.blurView.animator? is nil when you try to start it (have you verified that it isn't?).
Instead, your view class could look like this**:
final class BlurEffectView: UIVisualEffectView {
func fadeInEffect(_ completion: #escaping () -> Void) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0, options: []) {
self.effect = UIBlurEffect(style: .regular)
} completion: { _ in
completion()
}
}
}
And you would execute your animation like this:
present(vc, animated: false) { //vc presentation completion handler
vc.blurView.fadeInEffect {
// Completion here
}
}
*draw(_ rect:) gets called every time you let the system know that you need to redraw your view, and inside you're supposed to use CoreGraphics to draw the content of your view, which is not something you're doing here.
**Since you're not using any of the more advanced features of the property animator, it doesn't seem necessary to store it in an ivar.
The problem is that you are setting pausesOnCompletion to true. This causes the completion handler to not be called.
If you actually need that to be set to true, you need to use KVO to observe the isRunning property:
// property of your VC
var ob: NSKeyValueObservation?
...
self.ob?.invalidate()
self.ob = vc.blurView.animator?.observe(\.isRunning, options: [.new], changeHandler: { (animator, change) in
if !(change.newValue!) {
print("completed")
}
})
vc.blurView.animator?.startAnimation()
And as EmilioPelaez said, you shouldn't be initialising your animator in draw. Again, if you actually have a reason for using pausesOnCompletion = true, set those in a lazy property:
lazy var animator: UIViewPropertyAnimator? = {
let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in
self.effect = self.theEffect
}
anim.pausesOnCompletion = true
return anim
}()
self.effect = nil could be set in the initialiser.

Custom MapView Class Swift

I'm having some major issues and could use some help. I'm using some code from the company What3Words. They created a custom class:
open class MapView: MKMapView, MKMapViewDelegate, UIGestureRecognizerDelegate, CLLocationManagerDelegate {
//all their code is here
}
My goal is to use a tap gesture in order to acquire a lat/long from the map. I can do this incredibly easily when using a MapViewKit drop in and I make the object conform to MKMapView!
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func tapped(_ sender: UITapGestureRecognizer) {
print("hey")
let tap = sender
let coord = tap.location(in: map)
print(coord)
print(tap.location(in: self))
print(tap.location(in: map))
map.convert(coord, to: map)
print(map.convert(coord, to: map))
}
}
But, if I conform it MKMapView! I can't access any of the specialty code within the open class.
and if I conform it to the open class I don't know how to use my tap gesture.
I'm supposed to call this code below in whatever VC I want the map to appear in:
super.viewDidLoad()
let map = MapViewController(frame: view.frame)
map.set(api: api)
map.set(center: "school.truck.lunch", latitudeSpan: 0.05, longitudeSpan: 0.05)
map.showsUserLocation = true
map.onError = { error in
self.showError(error: error)
}
self.view = map
}
// MARK: Show an Error
/// display an error using a UIAlertController, error messages conform to CustomStringConvertible
func showError(error: Error) {
DispatchQueue.main.async {
let alert = UIAlertController(title: "Error", message: String(describing: error), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
}
Any idea how I can get a tap gesture to work while still using this special class? I feel like I've tried everything. UGH!! Please assist.
The simplest way to do it is to add the following to your ViewController:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let location = touches.first?.location(in: map) {
map.screenCoordsToWords(point: location) { square in
print(square.words ?? "")
}
}
}
And in your MapView class add this:
func screenCoordsToWords(point: CGPoint, completion: #escaping (W3WSquare) -> () ) {
let coordinates = convert(point, toCoordinateFrom: self)
self.api?.convertTo3wa(coordinates: coordinates, language: "en") { square, error in
if let s = square {
completion(s)
}
}
}
But doing it this way can lead to confusion between MKMapView's touch event handler and your view controller. Depending on what you're trying to accomplish this might be okay, or not okay.
If it's an issue, you can instead attach a gesture recognizer inside your MapView, but there is a trick to prevent overriding the MKMapView's built in double tap recogniser:
func addGesture() {
/// when the user taps the map this is called and it gets the square info
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
// A kind of tricky thing to make sure double tap doesn't trigger single tap in MKMapView
let doubleTap = UITapGestureRecognizer(target: self, action:nil)
doubleTap.numberOfTapsRequired = 2
addGestureRecognizer(doubleTap)
tap.require(toFail: doubleTap)
tap.delegate = self
addGestureRecognizer(tap)
}
#objc func tapped(_ gestureRecognizer : UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self)
let coordinates = convert(location, toCoordinateFrom: self)
api?.convertTo3wa(coordinates: coordinates, language: "en") { square, error in
if let s = square {
print(s.words ?? "")
}
}
}
Then in your ViewController's viewDidLoad, set it up:
map.addGesture()

iOS SwiftUI: How to detect location of UITapGesture on view?

Using Mapbox, the map struct conforms to the UIViewRepresentable protocol. In my makeUIView() function, I create a tap gesture recognizer and add it to the map view.
struct Map: UIViewRepresentable {
private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
func makeUIView(context: UIViewRepresentableContext<Map>) -> MGLMapView {
mapView.delegate = context.coordinator
let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: Selector("tappedMap"))
mapView.addGestureRecognizer(gestureRecognizer)
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<Map>) {
print("Just updated the mapView")
}
func makeCoordinator() -> Map.Coordinator {
Coordinator(appState: appState, self)
}
// other functions that make the struct have functions required by Mapbox
func makeCoordinator() -> Map.Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MGLMapViewDelegate {
var control: Map
//a bunch of delegate functions
#objc func tappedMap(sender: UITapGestureRecognizer) {
let locationInMap = sender.location(in: control)
let coordinateSet = sender.convert(locationInMap, toCoordinateFrom: control)
}
}
}
Neither of the lines in the tappedMap function compile properly...also, when I have 'sender: UITapGestureRecognizer' in the parameters of tappedMap, I causes the application to crash when I tap the mapView--If I remove the parameter, then the function is at least called properly without crashing. Please help
OK, the first problem is your selector definition in the tapGesture declaration. Change it to this:
let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tappedMap(sender:)))
Also, you need to manage the priority of gesture recognizers, since MGLMapView includes UIGestureRecognizer logic internally. I found a discussion of here:
Adding your own gesture recognizer to MGLMapView will block the
corresponding gesture recognizer built into MGLMapView. To avoid
conflicts, define which gesture takes precedence.
You can try this code in your project (pretty much just copied from the linked page above) EDIT: I needed to change the order of lines from my original answer:
let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: Selector("tappedMap"))
// HERE'S THE NEW STUFF:
for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
gestureRecognizer.require(toFail: recognizer)
}
mapView.addGestureRecognizer(gestureRecognizer)
EDIT: I was able to test this.
I'm a little confused by the logic inside your tappedMap selector: maybe you meant something like this?
#objc func tappedMap(sender: UITapGestureRecognizer) {
let locationInMap = sender.location(in: control.mapView)
let coordinateSet = sender.view?.convert(locationInMap, to: control.mapView)
}

TVOS : detecting touches with press began and functions (Swift Spritekit)

im trying to define touches in TVOS with press began but its not working.
i want to connect 3 functions
Start Game
Play Pause Music
Restart Game
Game scene TVOS:
func StartGameRecognizer(gesture: UITapGestureRecognizer) {
if isGameOver {
} else if !isStarted {
start()
} else {
hero.flip()
}
}
func playPauseMusicRecognizer(gesture: UITapGestureRecognizer) {
let onoroff = UserDefaults.standard.bool(forKey: "onoroff")
if !onoroff { //playing is false
Singleton.sharedInstance().pauseBackgroundMusic()
SoundOnOff.texture = SKTexture(imageNamed:"Sound-off.png")
UserDefaults.standard.set(true, forKey: "onoroff")
}
else {
Singleton.sharedInstance().resumeBackgroundMusic()
SoundOnOff.texture = SKTexture(imageNamed:"Sound-on.png")
UserDefaults.standard.set(false, forKey: "onoroff")
}
}
func RestartGameRecognizer(gesture: UISwipeGestureRecognizer){
print("RestartGame")
//Re-open GameScene
GameViewController().TitleGameOver.isHidden = true
GameViewController().RestartButton.isHidden = true
GameViewController().scoreTextLabel.isHidden = true
GameViewController().highscoreTextLabel.isHidden = true
GameViewController().ScoreBoardTV.isHidden = true
GameViewController().Score.isHidden = true
GameViewController().HighScore.isHidden = true
GameViewController().NewhighscoreTextLabel.isHidden = true
GameViewController().HomeButton.isHidden = true
// Singleton.sharedInstance().resumeSoundEffectClickedButton()
GameViewController().gameDidStart()
}
GameViewControllerTVOS:
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
for press in presses {
switch press.type {
case .upArrow:
print("Up Arrow")
case .downArrow:
print("Down arrow")
case .leftArrow:
print("Left arrow")
case .rightArrow:
print("Right arrow")
case .select:
print("Select")
case .menu:
print("Menu")
case .playPause:
print("Play/Pause")
default:
print("")
}
}
}
How i can use it right?
How can i transfer functions between scene to view controller?
I need example or hint to write the code right.
Update:
GameSceneTvOS:
override func didMove(to view: SKView) {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(GameSceneTVOS.StartGameRecognizer(gesture:)))
tapGesture.allowedPressTypes = [NSNumber(value: UIPressType.Select.rawValue)]
view.addGestureRecognizer(tapGesture)
let tapGesture1 = UITapGestureRecognizer(target: self, action: #selector(GameSceneTVOS.PlaypauseMusicRecognizer(gesture:)))
tapGesture1.allowedPressTypes = [NSNumber(value: UIPressType.PlayPause.rawValue)]
view.addGestureRecognizer(tapGesture1)
let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(GameSceneTVOS.RestartGameRecognizer(gesture:)))
swipeUp.direction = UISwipeGestureRecognizerDirection.up
self.view?.addGestureRecognizer(swipeUp)
}
**Functions :**
func StartGameRecognizer(gesture: UITapGestureRecognizer) {
print("StartGame")
if isGameOver {
} else if !isStarted {
start()
} else {
hero.flip()
}
}
func PlaypauseMusicRecognizer(gesture: UITapGestureRecognizer) {
print("PlaypauseMusic")
let onoroff = UserDefaults.standard.bool(forKey: "onoroff")
if !onoroff { //playing is false
Singleton.sharedInstance().pauseBackgroundMusic()
SoundOnOff.texture = SKTexture(imageNamed:"Sound-off.png")
UserDefaults.standard.set(true, forKey: "onoroff")
}
else {
Singleton.sharedInstance().resumeBackgroundMusic()
SoundOnOff.texture = SKTexture(imageNamed:"Sound-on.png")
UserDefaults.standard.set(false, forKey: "onoroff")
}
}
func RestartGameRecognizer(gesture: UISwipeGestureRecognizer){
print("RestartGame")
//Re-open GameScene
GameViewController().TitleGameOver.isHidden = true
GameViewController().RestartButton.isHidden = true
GameViewController().scoreTextLabel.isHidden = true
GameViewController().highscoreTextLabel.isHidden = true
GameViewController().ScoreBoardTV.isHidden = true
GameViewController().Score.isHidden = true
GameViewController().HighScore.isHidden = true
GameViewController().NewhighscoreTextLabel.isHidden = true
GameViewController().HomeButton.isHidden = true
// Singleton.sharedInstance().resumeSoundEffectClickedButton()
GameViewController().gameDidStart()
}
You code has some problems.
1) This code is wrong in the restartGame method.
GameViewController().TitleGameOver.isHidden = true
GameViewController().RestartButton.isHidden = true
...
You are creating a new instance of GameViewController on every line, you are not referencing the current game view controller.
2) You should not be using your GameViewController to create your UI, you should be doing it directly in the relevant SKScenes using only SpriteKit APIs (SKLabelNodes, SKSpriteNodes, SKNodes etc). Using UIKit in SpriteKit, except in some occasions, is bad practice.
3) You should be using TouchesBegan, TouchesMoved etc directly in the SKScenes to get touch input, dont use the GameViewController method.
They fill fire just like they do when you are on iOS.
You can also create gesture recognizers in your SKScene to get button presses from the SiriRemote.
/// Pressed, not tapped, main touch pad
let pressedMain = UITapGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
pressedMain.allowedPressTypes = [NSNumber(value: UIPressType.select.rawValue)]
view?.addGestureRecognizer(pressedMain)
/// Pressed play pause button
let pressedPlayPause = UITapGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
pressedPlayPause.allowedPressTypes = [NSNumber(value: UIPressType.playPause.rawValue)]
view?.addGestureRecognizer(pressedPlayPause)
/// Pressed menu button
let pressedMenu = UITapGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
pressedMenu.allowedPressTypes = [NSNumber(value: UIPressType.menu.rawValue)]
view?.addGestureRecognizer(pressedMenu)
You can also use swipe gesture recognizers if you want
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
rightSwipe.direction = .right
view?.addGestureRecognizer(rightSwipe)
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
leftSwipe.direction = .left
view?.addGestureRecognizer(leftSwipe)
let upSwipe = UISwipeGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
upSwipe.direction = .up
view?.addGestureRecognizer(upSwipe)
let downSwipe = UISwipeGestureRecognizer(target: self, action: #selector(SOMEMETHOD))
downSwipe.direction = .down
view?.addGestureRecognizer(downSwipe)
If you are using gesture recognisers, remember they are added to the GameViewController (view?.addGesture...) so its good practice to remove them when you add either new ones, or if you change to a new scene where you might need different ones.
Call this code when you exit a scene or add new gesture recognizers.
for gestureRecognizer in view?.gestureRecognizers ?? [] {
view?.removeGestureRecognizer(gestureRecognizer)
}
If you are looking for fully fledged micro gamepad support than you will need to watch some tutorials about the gameController framework.
4) Try putting your string keys like the ones for UserDefaults in some property.
enum Key: String {
case onoroff
}
and than use it like so
UserDefaults.standard.set(true, forKey: Key.onoroff.rawValue)
to avoid making typos.
5) You should be following the Swift conventions consistently, some of your methods and properties start with capital letters but they shouldn't.
I would advise that you restructure your code and not continue with this approach of trying to use the GameViewController for all this. It should be all done directly in the relevant SKScene.
EDIT. I think you are calling the selector wrong, try this. When your function has a parameter you would use this (_:), you are trying to use (gesture:). Try this instead.
... action: #selector(startGameRecognizer(_:))
Hope this helps