I currently have the problem that touches are not always identified correctly,
My goal is to have 3 gestures,The 3 gestures are
A user can tap on a view and the tap gets recognised,
A user can double tap on a view and the double tap is recognised,
A user can move their finger on the screen and if a view is below it
a tab is recognised.
However I have multiple views all animating constantly and they may overlap,
Currently I sort views by size and have the smallest views on top of larger views.
And I typically get an issue that UIViews are not recognised when tapping on them. In particular double taps, swiping seems to work fine most of the time however the whole experience is very inconsistent.
The current code I'm using to solve the problem is:
class FinderrBoxView: UIView {
private var lastBox: String?
private var throttleDelay = 0.01
private var processQueue = DispatchQueue(label: "com.finderr.FinderrBoxView")
public var predictedObjects: [FinderrItem] = [] {
didSet {
predictedObjects.forEach { self.checkIfBoxIntersectCentre(prediction: $0) }
drawBoxs(with: FinderrBoxView.sortBoxByeSize(predictedObjects))
setNeedsDisplay()
}
}
func drawBoxs(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var views = subviews.compactMap { $0 as? BoxView }
views = views.filter { view in
guard let closest = newBoxes.sorted(by: { x, y in
let xd = FinderrBoxView.distanceBetweenBoxes(view.frame, x.box)
let yd = FinderrBoxView.distanceBetweenBoxes(view.frame, y.box)
return xd < yd
}).first else { return false }
if FinderrBoxView.updateOrCreateNewBox(view.frame, closest.box)
{
newBoxes.remove(closest)
UIView.animate(withDuration: self.throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = closest.box
}, completion: nil)
return false
} else {
return true
}
}
views.forEach { $0.removeFromSuperview() }
newBoxes.forEach { self.createLabelAndBox(prediction: $0) }
accessibilityElements = subviews
}
func update(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var viewsToRemove = [UIView]()
for view in subviews {
var shouldRemoveView = true
for box in predictions {
if FinderrBoxView.updateOrCreateNewBox(view.frame, box.box)
{
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = box.box
}, completion: nil)
shouldRemoveView = false
newBoxes.remove(box)
}
}
if shouldRemoveView {
viewsToRemove.append(view)
}
}
viewsToRemove.forEach { $0.removeFromSuperview() }
for prediction in newBoxes {
createLabelAndBox(prediction: prediction)
}
accessibilityElements = subviews
}
func checkIfBoxIntersectCentre(prediction: FinderrItem) {
let centreX = center.x
let centreY = center.y
let maxX = prediction.box.maxX
let minX = prediction.box.midX
let maxY = prediction.box.maxY
let minY = prediction.box.minY
if centreX >= minX, centreX <= maxX, centreY >= minY, centreY <= maxY {
// NotificationCenter.default.post(name: .centreIntersectsWithBox, object: prediction.name)
}
}
func removeAllSubviews() {
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear) {
for i in self.subviews {
i.frame = CGRect(x: i.frame.midX, y: i.frame.midY, width: 0, height: 0)
}
} completion: { _ in
self.subviews.forEach { $0.removeFromSuperview() }
}
}
static func getDistanceFromCloseBbox(touchAt p1: CGPoint, items: [FinderrItem]) -> Float {
var boxCenters = [Float]()
for i in items {
let distance = Float(sqrt(pow(i.box.midX - p1.x, 2) + pow(i.box.midY - p1.y, 2)))
boxCenters.append(distance)
}
boxCenters = boxCenters.sorted { $0 < $1 }
return boxCenters.first ?? 0.0
}
static func sortBoxByeSize(_ items: [FinderrItem]) -> [FinderrItem] {
return items.sorted { i, j -> Bool in
let iC = sqrt(pow(i.box.height, 2) + pow(i.box.width, 2))
let jC = sqrt(pow(j.box.height, 2) + pow(j.box.width, 2))
return iC > jC
}
}
static func updateOrCreateNewBox(_ box1: CGRect, _ box2: CGRect) -> Bool {
let distance = sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2))
print(distance)
return distance < 50
}
static func distanceBetweenBoxes(_ box1: CGRect, _ box2: CGRect) -> Float {
return Float(sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2)))
}
func createLabelAndBox(prediction: FinderrItem) {
let bgRect = prediction.box
let boxView = BoxView(frame: bgRect ,itemName: "box")
addSubview(boxView)
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
func processTouches(_ touches: Set<UITouch>, with event: UIEvent?) {
if UIAccessibility.isVoiceOverRunning { return }
if predictedObjects.count == 0 { return }
if let touch = touches.first {
let hitView = hitTest(touch.location(in: self), with: event)
if hitView?.accessibilityLabel == lastBox { return }
lastBox = hitView?.accessibilityLabel
guard let boxView = hitView as? BoxView else {
return
}
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear) {
boxView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
} completion: { _ in
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear, animations: {
boxView.backgroundColor = UIColor.clear
}, completion: nil)
}
}
}
}
class BoxView: UIView {
let id = UUID()
var itemName: String
init(frame: CGRect, itemName: String) {
self.itemName = itemName
super.init(frame: frame)
if !UIAccessibility.isVoiceOverRunning {
let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
target: self,
singleAction: #selector(handleDoubleTapGesture),
doubleAction: #selector(handleDoubleTapGesture)
)
addGestureRecognizer(singleDoubleTapRecognizer)
}
}
#objc func navigateAction() -> Bool {
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
return true
}
required init?(coder aDecoder: NSCoder) {
itemName = "error aDecoder"
super.init(coder: aDecoder)
}
#objc func handleDoubleTapGesture(_: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
}
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var timeout: TimeInterval = 0.5 {
didSet {
targetDelegate.timeout = timeout
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
weak var target: AnyObject?
var singleAction: Selector
var doubleAction: Selector
var timeout: TimeInterval = 0.5
var tapCount = 0
var workItem: DispatchWorkItem?
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
#objc func recognizerAction(recognizer: UITapGestureRecognizer) {
tapCount += 1
if tapCount == 1 {
workItem = DispatchWorkItem { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
DispatchQueue.main.asyncAfter(
deadline: .now() + timeout,
execute: workItem!
)
} else {
workItem?.cancel()
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
}
}
}
class FinderrItem: Equatable, Hashable {
var box: CGRect
init(
box: CGRect)
{
self.box = box
}
func hash(into hasher: inout Hasher) {
hasher.combine(Float(box.origin.x))
hasher.combine(Float(box.origin.y))
hasher.combine(Float(box.width))
hasher.combine(Float(box.height))
hasher.combine(Float(box.minX))
hasher.combine(Float(box.maxY))
}
static func == (lhs: FinderrItem, rhs: FinderrItem) -> Bool {
return lhs.box == rhs.box
}
}
By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:
UIView.animate(withDuration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})
Related
I have a SwiftUI application which has a carousel of videos. I'm using an AVPlayer with UIViewRepresentable and I'm creating the carousel with a ForEach loop of my custom UIViewRepresentable view. I want to have a "periodicTimeObserver" on the active AVPlayer, but it crashes and says
"An instance of AVPlayer cannot remove a time observer that was added
by a different instance of AVPlayer SwiftUI"
My question is how can I remove the periodicTimeObserver of an AVPlayer inside of a UIViewRepresentable inside of a UIView, without causing the app to crash?
Here is my code:
ForEach(videosArray.indices, id: \.self) { i in
let videoURL = videosArray[i]
ZStack {
VStack {
VideoView.init(viewModel: viewModel, videoURL: URL(string: videoURL)!, videoIndex: i)
}
}
}
struct VideoView: UIViewRepresentable {
#ObservedObject var viewModel = viewModel.init()
var videoURL:URL
var previewLength:Double?
var videoIndex: Int
func makeUIView(context: Context) -> UIView {
return PlayerView.init(frame: .zero, url: videoURL, previewLength: previewLength ?? 6)
}
func updateUIView(_ uiView: UIView, context: Context) {
if videoIndex == viewModel.currentIndexSelected {
if let playerView = uiView as? PlayerView {
if !viewModel.isPlaying {
playerView.pause()
} else {
playerView.play(customStartTime: viewModel.newStartTime, customEndTime: viewModel.newEndTime)
}
}
} else {
if let playerView = uiView as? PlayerView {
playerView.pause()
}
}
}
}
public class ViewModel: ObservableObject {
#Published public var currentIndexSelected: Int = 0
#Published public var isPlaying: Bool = true
#Published public var newStartTime = 0.0
#Published public var newEndTime = 30.0
}
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
var player: AVPlayer?
var timeObserver: Any? = nil
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
player = AVPlayer(url: url)
player!.volume = 0
player!.play()
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
self.previewLength = 15
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func pause() {
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
player?.pause()
}
#objc func replayFinishedItem(noti: NSNotification) {
print("REPLAY FINISHED NOTIIIII: \(noti)")
if let timeDict = noti.object as? [String: Any], let startTime = timeDict["startTime"] as? Double, let endTime = timeDict["endTime"] as? Double/*, let player = timeDict["player"] as? AVPlayer, let observer = timeDict["timeObserver"]*/ {
self.removeTheTimeObserver()
self.play(customStartTime: startTime, customEndTime: endTime)
}
}
#objc func removeTheTimeObserver() {
print("ATTEMPT TO REMOVE IT!")
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
func play(at playPosition: Double = 0.0, customStartTime: Double = 0.0, customEndTime: Double = 15.0) {
var startTime = customStartTime
var endTime = customEndTime
if customStartTime > customEndTime {
startTime = customEndTime
endTime = customStartTime
}
if playPosition != 0.0 {
player?.seek(to: CMTime(seconds: playPosition, preferredTimescale: CMTimeScale(1)))
} else {
player?.seek(to: CMTime(seconds: startTime, preferredTimescale: CMTimeScale(1)))
}
player?.play()
var timeDict: [String: Any] = ["startTime": startTime, "endTime": endTime]
NotificationCenter.default.addObserver(self, selector: #selector(self.replayFinishedItem(noti:)), name: .customAVPlayerShouldReplayNotification, object: nil)
self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 100), queue: DispatchQueue.main, using: { [weak self] time in
guard let strongSelf = self else {
return
}
let currentTime = CMTimeGetSeconds(strongSelf.player!.currentTime())
let currentTimeStr = String(currentTime)
if let currentTimeDouble = Double(currentTimeStr) {
let userDefaults = UserDefaults.standard
userDefaults.set(currentTimeDouble, forKey: "currentTimeDouble")
NotificationCenter.default.post(name: .currentTimeDouble, object: currentTimeDouble)
if currentTimeDouble >= endTime {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
strongSelf.player?.pause()
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
} else if let currentItem = strongSelf.player?.currentItem {
let seconds = currentItem.duration.seconds
if currentTimeDouble >= seconds {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
}
}
}
})
}
}
As you can see I am setting the presentation style as .overCurrentContext.
extension SingleQuestionViewController: AddResponseDelegate {
func save(response text: String, questionID: Int) {
questionsWrapper.add(newResponse: text, questionID: questionID) { [weak self] successful in
if successful {
self?.responseTV?.safelyReload()
} else {
DispatchQueue.main.async {
let alertViewController = AlertViewController<Any>()
alertViewController.modalPresentationStyle = .overCurrentContext
let contentModel = RegularContentsModel(title: "controllerTitle", message: "message")
let authorizeButtonModel = SimpleButtonModel(title: "yes message", action: {
//action goes here
})
let doNothingButtonModel = SimpleButtonModel(title: "noMsg", action: {
//completion?()
})
alertViewController.styleRegular(regularContentsModel: contentModel,
models: [authorizeButtonModel, doNothingButtonModel])
self?.present(alertViewController, animated: false, completion: nil)
}
}
}
questionsWrapper.goToQuestion(with: questionID)
responseTV?.safelyReload()
}
}
Here is the result:
I don't think this is caused by it being on the background thread, because if I move it to viewDidLoad, then I get the same result:
override func viewDidLoad() {
super.viewDidLoad()
setUpTopBar()
setupSearchBar()
responseTV.showsVerticalScrollIndicator = false
setupArrows()
responseTV.register(SimpleCell.self, forCellReuseIdentifier: SimpleCell.reuseID)
setAccessibility()
let alertViewController = AlertViewController<Any>()
alertViewController.modalPresentationStyle = .overCurrentContext
let contentModel = RegularContentsModel(title: "controllerTitle", message: "message")
let authorizeButtonModel = SimpleButtonModel(title: "yes message", action: {
//action goes here
})
let doNothingButtonModel = SimpleButtonModel(title: "noMsg", action: {
//completion?()
})
alertViewController.styleRegular(regularContentsModel: contentModel,
models: [authorizeButtonModel, doNothingButtonModel])
self.present(alertViewController, animated: false, completion: nil)
}
Here is the implementation of my custom alert.
class AlertViewController<Payload>: AkinVC {
typealias FlagsAction = ([ReportFlag], Payload) -> Void
enum AlertStyle<Payload> {
case flag(FlagsAction)
}
let innerWholeAlertContainer = UIView()
let outerWholeAlertContainer = UIView()
let buttonStack = AlertButtonsStack()
var payload: Payload?
let transitionDuration: TimeInterval = 0.11
let containerWidth: CGFloat = 300
private var contentsView: UIView! {
didSet {
innerWholeAlertContainer.addSubview(contentsView)
contentsView.constraints(firstHorizontal: .distanceToLeading(innerWholeAlertContainer.leadingAnchor, 0),
secondHorizontal: .distanceToTrailing(innerWholeAlertContainer.trailingAnchor, 0),
vertical: .distanceToTop(innerWholeAlertContainer.topAnchor, 0),
secondVertical: .distanceToBottom(buttonStack.topAnchor, 0))
}
}
func styleNoButtons(regularContentsModel: RegularContentsModel) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
setButtonConstraints()
}
func styleAsFlagView(flagsAction: #escaping FlagsAction) {
initialSetup()
let stackView = FlagsStackView()
stackView.flagItemViews = [FlagItemView](ReportFlag.allCases)
contentsView = stackView
buttonStack.buttonModels(
ButtonModel(tekt: "Report", color: .romanceRed, tektColor: .white, action: { [weak stackView] in
guard let selectedFlags = stackView?.flagItemViews?.selectedFlags,
let payload = self.payload else { return }
flagsAction(selectedFlags, payload)
self.dismissAlert()
}),
ButtonModel(tekt: "Cancel", color: .white, tektColor: .black,
borders: BorderModel(color: UIColor.black.withAlphaComponent(0.16), width: 1, edges: [.top]),
action: { [weak self] in
self?.dismissAlert()
})
)
setButtonConstraints()
}
func styleAsOkayAlert(regularContentsModel: RegularContentsModel, action: Action? = nil) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
let okayModel = standardizeButtonsWithDismissAction(models: [SimpleButtonModel(title: "Okay, I got it.", action: action)])
buttonStack.buttonModels(okayModel)
setButtonConstraints()
}
func styleCancelAlert(regularContentsModel: RegularContentsModel, models: SimpleButtonModel...) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
var models = models
models.append(SimpleButtonModel(title: "Cancel"))
let newButtonModels = standardizeButtonsWithDismissAction(models: models)
buttonStack.buttonModels(newButtonModels)
setButtonConstraints()
}
func styleRegular(regularContentsModel: RegularContentsModel, models: SimpleButtonModel...) {
self.styleRegular(regularContentsModel: regularContentsModel, models: models)
}
func styleRegular(regularContentsModel: RegularContentsModel, models: [SimpleButtonModel]) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
let newButtonModels = standardizeButtonsWithDismissAction(models: models)
buttonStack.buttonModels(newButtonModels)
setButtonConstraints()
}
private func standardizeButtonsWithDismissAction(models: [SimpleButtonModel]) -> [ButtonModel] {
var buttonModelsToAdd: [ButtonModel] = []
let count = models.count
for (inde, model) in models.enumerated() {
var borders: [BorderModel] = []
if count > 2 || count == 1 {
borders.append(BorderModel(color: .lightGray, width: 1, edges: [.top]))
} else if count == 2 {
if inde == 0 {
borders.append(BorderModel(color: .lightGray, width: 1, edges: [.top]))
} else if inde == 1 {
borders.append(BorderModel(color: .lightGray, width: 1, edges: [.left, .top]))
}
}
buttonModelsToAdd.append(ButtonModel(tekt: model.title, color: .white, tektColor: .darkGray, borders: borders, action: {
model.action?()
self.dismissAlert()
}))
}
return buttonModelsToAdd
}
func dismissAlert() {
UIView.animate(withDuration: transitionDuration, animations: {
self.view.alpha = 0
}) { (completed) in
self.safelyDissmiss(animated: false)
}
}
fileprivate func initialSetup() {
self.view.alpha = 0
modalPresentationStyle = .currentContext
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
view.addSubview(outerWholeAlertContainer)
outerWholeAlertContainer.addSubview(innerWholeAlertContainer)
outerWholeAlertContainer.pinToEdges(innerWholeAlertContainer)
innerWholeAlertContainer.backgroundColor = .white
innerWholeAlertContainer.addSubview(buttonStack)
outerWholeAlertContainer.constraints(.horizontal(.centeredHorizontallyWith(view)),
.vertical(.centeredVerticallyTo(view)),
.horizontal(.width(containerWidth)) )
}
func setButtonConstraints() {
buttonStack.constraints(.horizontal(.distanceToLeading(innerWholeAlertContainer.leadingAnchor, 0)),
.horizontal(.distanceToTrailing(innerWholeAlertContainer.trailingAnchor, 0)),
.vertical(.distanceToBottom(innerWholeAlertContainer.bottomAnchor, 0)))
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
outerWholeAlertContainer.layer.applySketchShadow()
innerWholeAlertContainer.roundCorners(constant: 15)
UIView.animate(withDuration: transitionDuration, animations: {
self.view.alpha = 1
})
}
}
Here is what the visual debugger shows:
The view controller:
creates the alert controller;
sets modalPresentationStyle to .overCurrentContext; and
and calls styleRegular.
But styleRegular:
calls initialSetup
which resets modalPresentationStyle to .currentContext
It's that last step that is discarding your previous .overCurrentContext setting.
I have created a circular progress bar in Swift that animated over 1.5 seconds to value 1 when user hold on view. But I want to add a new viewcontroller when animation is done and restart my circular progressbar if user ended to early. Can someone help me?
Circulars progress bar is working with animation when user hold on view and stop at release.
class CounterView: UIView {
var bgPath: UIBezierPath!
var shapeLayer: CAShapeLayer!
var progressLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
bgPath = UIBezierPath()
self.simpleShape()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
bgPath = UIBezierPath()
self.simpleShape()
}
func simpleShape()
{
createCirclePath()
shapeLayer = CAShapeLayer()
shapeLayer.path = bgPath.cgPath
shapeLayer.lineWidth = 5
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.clear.cgColor
progressLayer = CAShapeLayer()
progressLayer.path = bgPath.cgPath
progressLayer.lineCap = kCALineCapRound
progressLayer.lineWidth = 5
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.yellow.cgColor
progressLayer.strokeEnd = 0.0
self.layer.addSublayer(shapeLayer)
self.layer.addSublayer(progressLayer)
}
private func createCirclePath()
{
let x = self.frame.width/2
let y = self.frame.height/2
let center = CGPoint(x: x, y: y)
print(x,y,center)
bgPath.addArc(withCenter: center, radius: x/CGFloat(2), startAngle: CGFloat(0), endAngle: CGFloat(6.28), clockwise: true)
bgPath.close()
}
var animationCompletedCallback: ((_ isAnimationCompleted: Bool) -> Void)?
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
CATransaction.setCompletionBlock {
if let callBack = self.animationCompletedCallback { callBack(true) }
}
CATransaction.begin()
let animation = CABasicAnimation (keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = value
animation.repeatCount = 1
animation.timingFunction = CAMediaTimingFunction (name: kCAMediaTimingFunctionLinear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(animation, forKey: "animateprogress")
CATransaction.commit()
}
func removeLayers() {
shapeLayer.removeAllAnimations()
shapeLayer.removeFromSuperlayer()
progressLayer.removeAllAnimations()
progressLayer.removeFromSuperlayer()
}
}
class ViewController: UIViewController {
#IBOutlet weak var counterView: CounterView!
#IBOutlet weak var holdView: UIView!
var isAnimationCompleted = false
override func viewDidLoad() {
super.viewDidLoad()
addLongPressGesture()
addCounterViewCallback()
}
#objc func longPress(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
// self.counterView.simpleShape()
self.counterView.setProgressWithAnimation(duration: 1.5, value: 1.0)
}
if gesture.state == UIGestureRecognizerState.ended {
if !isAnimationCompleted {
self.counterView.removeLayers()
}
}
}
func addLongPressGesture(){
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gesture:)))
lpgr.minimumPressDuration = 0
self.holdView.addGestureRecognizer(lpgr)
}
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] (isCompleted) in
guard let weakSelf = self else {return}
weakSelf.isAnimationCompleted = isCompleted
weakSelf.addFlashView()
}
}
func addFlashView(){
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let resultViewController = storyBoard.instantiateViewController(withIdentifier: "ResultView") as! Flash
self.present(resultViewController, animated:true, completion:nil)
}
Add new viewcontroller when animation is done and restart animation if user release view and hold on it again.
Add a callback to know when animation is ended. And use CATransaction to know when animation is completed.
var animationCompletedCallback: (() -> Void)?
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
CATransaction.setCompletionBlock {
if let callBack = animationCompletedCallback {
callBack()
}
}
CATransaction.begin()
let animation = CABasicAnimation (keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = value
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction (name: kCAMediaTimingFunctionLinear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(animation, forKey: "animateprogress")
CATransaction.commit()
}
And add this function after addLongPressGesture() in viewDidLoad() :
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] in
guard let weakSelf = self else {return}
weakSelf.addFlashView()
}
}
To remove layer use this:
func removeLayers() {
shapeLayer.removeAllAnimations()
shapeLayer.removeFromSuperlayer()
progressLayer.removeAllAnimations()
progressLayer.removeFromSuperlayer()
}
Update 1:
To remove animation if user stops pressing, you need can add on variable in callback like this :
var animationCompletedCallback: ((isAnimationCompleted: Bool) -> Void)?
So now callback in CounterView will be :
if let callBack = animationCompletedCallback { callBack(true) }
In your controller add one variable:
var isAnimationCompleted = false
Change addCounterViewCallback() :
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] (isCompleted) in
guard let weakSelf = self else {return}
weakSelf.isAnimationCompleted = isCompleted
weakSelf.addFlashView()
}
}
Now you can add one condition in your longPress():
if gesture.state == UIGestureRecognizerState.ended {
if !isAnimationCompleted {
//Call remove layers code
}
}
Update 2:
Add a variable in CounterView:
var isAnimationCompleted = true
Change callback like this :
CATransaction.setCompletionBlock {
if let callBack = self.animationCompletedCallback { callBack(isAnimationCompleted) }
}
In controller longPress() :
if gesture.state == UIGestureRecognizerState.ended {
if !isAnimationCompleted {
self.counterView.isAnimationCompleted = false
self.counterView.removeLayers()
}
}
Modify addCounterViewCallback() to this:
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] (isCompleted) in
guard let weakSelf = self else {return}
weakSelf.isAnimationCompleted = isCompleted
if isCompleted {
weakSelf.addFlashView()
}
}
}
Im trying to get the player sprite in my game to animate according to what it is doing, ie.. walking, attacking, resting.
I have the following files AgentNode and GameScene.
The sprite spawns and moves around as I want, just with the initial action that was in place when the sprite was added to the scene.
for example if I add the sprite with the "restingFrames" animation, it spawns correctly but when the sprite is moved, the animation is still using "restingFrames" and not "walkingFrames" as I want. Can't figure it out, please help.
AgentNode.swift
import SpriteKit
import GameplayKit
class AgentNode: SKNode, GKAgentDelegate {
var agent = GKAgent2D()
var triangleShape = SKShapeNode()
var player = SKSpriteNode()
var walkingFrames: [SKTexture] = []
var restingFrames: [SKTexture] = []
var attackingFrames: [SKTexture] = []
var firstFrameTexture: SKTexture = SKTexture()
var playerSpawned = false
override init() {
super.init()
}
init(scene:SKScene, radius: Float, position: CGPoint) {
super.init()
self.position = position
self.zPosition = 10
scene.addChild(self)
agent.radius = radius
agent.position = simd_float2(Float(position.x), Float(position.y))
agent.delegate = self
agent.maxSpeed = 100 * 2
agent.maxAcceleration = 500 * 4
}
func setupPlayer() {
setupRestingPlayerAnimation()
setupWalkingPlayerAnimation()
setupAttackingPlayerAnimation()
}
func setupWalkingPlayerAnimation() {
let walkingPlayerAtlas = SKTextureAtlas(named: "WalkingPlayer")
let numImages = walkingPlayerAtlas.textureNames.count
for i in 1...numImages {
let walkingPlayerTextureName = "walk_front\(i)"
walkingFrames.append(walkingPlayerAtlas.textureNamed(walkingPlayerTextureName))
}
}
func setupRestingPlayerAnimation() {
let restingPlayerAtlas = SKTextureAtlas(named: "RestingPlayer")
let numImages = restingPlayerAtlas.textureNames.count
for i in 1...numImages {
let restingPlayerTextureName = "still_frame\(i)"
restingFrames.append(restingPlayerAtlas.textureNamed(restingPlayerTextureName))
}
}
func setupAttackingPlayerAnimation() {
let attackingPlayerAtlas = SKTextureAtlas(named: "AttackingPlayer")
let numImages = attackingPlayerAtlas.textureNames.count
for i in 1...numImages {
let attackingPlayerTextureName = "attack_frame\(i)"
attackingFrames.append(attackingPlayerAtlas.textureNamed(attackingPlayerTextureName))
}
}
func animatePlayer() {
var restingSequence = SKAction()
var walkingSequence = SKAction()
var attackingSequence = SKAction()
let restingAnimation = SKAction.animate(with: restingFrames, timePerFrame: 0.15)
let walkingAnimation = SKAction.animate(with: walkingFrames, timePerFrame: 0.1)
let attackingAnimation = SKAction.animate(with: attackingFrames, timePerFrame: 0.1)
restingSequence = SKAction.sequence([restingSequence])
walkingSequence = SKAction.sequence([walkingSequence])
attackingSequence = SKAction.sequence([attackingSequence])
if isSeeking && !isAttacking{
firstFrameTexture = walkingFrames[0]
player.run(SKAction.repeatForever(walkingAnimation), withKey: "walkingAction")
print("walking")
} else if isAttacking && !isSeeking {
firstFrameTexture = attackingFrames[0]
player.run(SKAction.repeatForever(attackingAnimation), withKey: "attackingAction")
print("attacking")
} else {
firstFrameTexture = restingFrames[0]
player.run(SKAction.repeatForever(restingAnimation), withKey: "restingAction")
print("resting")
}
}
func addPlayerToScene() {
player = SKSpriteNode(texture: firstFrameTexture)
player.position = CGPoint(x: (frame.midX), y: (frame.midY))
player.setScale(1.5)
player.zRotation = CGFloat(Double.pi / 2.0)
player.zPosition = 10
self.addChild(player)
playerSpawned = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func agentWillUpdate(_ agent: GKAgent) {
}
func agentDidUpdate(_ agent: GKAgent) {
if playerSpawned == false {
print("player has not spawned")
setupPlayer()
animatePlayer()
addPlayerToScene()
playerSpawned = true
} else {
print("player has spawned")
animatePlayer()
}
print("player isSeeking \(isSeeking)")
print("player isAttacking \(isAttacking)")
guard let agent2D = agent as? GKAgent2D else {
return
}
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y))
self.zRotation = CGFloat(agent2D.rotation)
}
}
GameScene.swift
import SpriteKit
import GameplayKit
var isSeeking: Bool = false
var isAttacking: Bool = false
class GameScene: SKScene {
let trackingAgent = GKAgent2D()
var player = AgentNode()
var seekGoal : GKGoal = GKGoal()
let stopGoal = GKGoal(toReachTargetSpeed: 0.0)
var seeking : Bool = false {
willSet {
if newValue {
self.player.agent.behavior?.setWeight(5, for: seekGoal)
self.player.agent.behavior?.setWeight(0, for: stopGoal)
} else {
self.player.agent.behavior?.setWeight(0, for: seekGoal)
self.player.agent.behavior?.setWeight(5, for: stopGoal)
}
}
}
var agentSystem = GKComponentSystem()
var lastUpdateTime: TimeInterval = 0
override func didMove(to view: SKView) {
super.didMove(to: view)
self.trackingAgent.position = simd_float2(Float(self.frame.midX), Float(self.frame.midY))
self.agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
self.player = AgentNode(scene: self, radius: Float(20.0), position: CGPoint(x: self.frame.midX, y: self.frame.midY))
self.player.agent.behavior = GKBehavior()
self.agentSystem.addComponent(self.player.agent)
self.seekGoal = GKGoal(toSeekAgent: self.trackingAgent)
}
override func update(_ currentTime: CFTimeInterval) {
isSeeking = self.seeking
if lastUpdateTime == 0 {
lastUpdateTime = currentTime
}
let delta = currentTime - lastUpdateTime
lastUpdateTime = currentTime
self.agentSystem.update(deltaTime: delta)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.seeking = true
handleTouch(touches: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
self.seeking = false
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.seeking = false
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
handleTouch(touches: touches)
}
func handleTouch(touches:Set<UITouch>) {
guard let touch = touches.first
else {
return
}
let location = touch.location(in: self)
self.trackingAgent.position = simd_float2(Float(location.x), Float(location.y))
}
}
Well I figured it out. I updated the code as follows and everything is working great.
AgentNode.swift
import SpriteKit
import GameplayKit
class AgentNode: SKNode, GKAgentDelegate {
var agent = GKAgent2D()
var triangleShape = SKShapeNode()
var player = SKSpriteNode()
var walkingFrames: [SKTexture] = []
var restingFrames: [SKTexture] = []
var attackingFrames: [SKTexture] = []
var playerSpawned = false
override init() {
super.init()
}
init(scene:SKScene, radius: Float, position: CGPoint) {
super.init()
self.position = position
self.zPosition = 10
scene.addChild(self)
agent.radius = radius
agent.position = simd_float2(Float(position.x), Float(position.y))
agent.delegate = self
agent.maxSpeed = 100 * 2
agent.maxAcceleration = 500 * 4
}
func setupPlayer() {
setupRestingPlayerAnimation()
setupWalkingPlayerAnimation()
setupAttackingPlayerAnimation()
}
func setupWalkingPlayerAnimation() {
let walkingPlayerAtlas = SKTextureAtlas(named: "WalkingPlayer")
let numImages = walkingPlayerAtlas.textureNames.count
for i in 1...numImages {
let walkingPlayerTextureName = "walk_front\(i)"
walkingFrames.append(walkingPlayerAtlas.textureNamed(walkingPlayerTextureName))
}
}
func setupRestingPlayerAnimation() {
let restingPlayerAtlas = SKTextureAtlas(named: "RestingPlayer")
let numImages = restingPlayerAtlas.textureNames.count
for i in 1...numImages {
let restingPlayerTextureName = "still_frame\(i)"
restingFrames.append(restingPlayerAtlas.textureNamed(restingPlayerTextureName))
}
}
func setupAttackingPlayerAnimation() {
let attackingPlayerAtlas = SKTextureAtlas(named: "AttackingPlayer")
let numImages = attackingPlayerAtlas.textureNames.count
for i in 1...numImages {
let attackingPlayerTextureName = "attack_frame\(i)"
attackingFrames.append(attackingPlayerAtlas.textureNamed(attackingPlayerTextureName))
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func agentWillUpdate(_ agent: GKAgent) {
}
func agentDidUpdate(_ agent: GKAgent) {
guard let agent2D = agent as? GKAgent2D else {
return
}
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y))
self.zRotation = CGFloat(agent2D.rotation)
}
}
GameScene.swift
import SpriteKit
import GameplayKit
var isSeeking: Bool = false
var isAttacking: Bool = false
class GameScene: SKScene {
let trackingAgent = GKAgent2D()
var player = AgentNode()
var seekGoal : GKGoal = GKGoal()
let stopGoal = GKGoal(toReachTargetSpeed: 0.0)
var seeking : Bool = false {
willSet {
if newValue {
self.player.agent.behavior?.setWeight(5, for: seekGoal)
self.player.agent.behavior?.setWeight(0, for: stopGoal)
} else {
self.player.agent.behavior?.setWeight(0, for: seekGoal)
self.player.agent.behavior?.setWeight(5, for: stopGoal)
}
}
}
var agentSystem = GKComponentSystem()
var lastUpdateTime: TimeInterval = 0
override func didMove(to view: SKView) {
super.didMove(to: view)
self.trackingAgent.position = simd_float2(Float(self.frame.midX), Float(self.frame.midY))
self.agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
self.player = AgentNode(scene: self, radius: Float(20.0), position: CGPoint(x: self.frame.midX, y: self.frame.midY))
self.player.agent.behavior = GKBehavior()
self.agentSystem.addComponent(self.player.agent)
self.seekGoal = GKGoal(toSeekAgent: self.trackingAgent)
player.setupPlayer()
//player.animatePlayer()
player.player = SKSpriteNode(imageNamed: "default_pose")
player.player.position = CGPoint(x: (frame.midX), y: (frame.midY))
player.player.setScale(1.5)
player.player.zRotation = CGFloat(Double.pi / 2.0)
player.player.zPosition = 10
player.player.run(SKAction.repeatForever(SKAction.animate(with: player.restingFrames, timePerFrame: 0.2)))
player.addChild(player.player)
}
override func update(_ currentTime: CFTimeInterval) {
isSeeking = self.seeking
if lastUpdateTime == 0 {
lastUpdateTime = currentTime
}
let delta = currentTime - lastUpdateTime
lastUpdateTime = currentTime
self.agentSystem.update(deltaTime: delta)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.seeking = true
player.player.run(SKAction.repeatForever(SKAction.animate(with: player.walkingFrames, timePerFrame: 0.1)))
handleTouch(touches: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
self.seeking = false
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.seeking = false
player.player.removeAllActions()
player.player.run(SKAction.repeatForever(SKAction.animate(with: player.attackingFrames, timePerFrame: 0.1)))
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
handleTouch(touches: touches)
}
func handleTouch(touches:Set<UITouch>) {
guard let touch = touches.first
else {
return
}
let location = touch.location(in: self)
self.trackingAgent.position = simd_float2(Float(location.x), Float(location.y))
}
}
everyone i've been tearing out my hair trying to find a solution to an interactive view controller transition where you use the pan gesture in the downward direction to bring a full screen view controller from the top to the bottom. Has anyone run across or created any code like this. Below is my code. I already have the dismiss gesture down but cant figure out how to present the view controller by swiping down on the screen. PLEASE HELP!!!
import UIKit
class ViewController: UIViewController {
let interactor = Interactor()
var interactors:Interactor? = nil
let Mview = ModalViewController()
let mViewT: ModalViewController? = nil
var presentedViewControllers: UIViewController?
override func viewDidLoad() {
Mview.transitioningDelegate = self
Mview.modalPresentationStyle = .FullScreen
}
#IBAction func cameraSlide(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
// convert y-position to downward pull progress (percentage)
let translation = sender.translationInView(Mview.view)
let verticalMovement = translation.y / UIScreen.mainScreen().bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactors else { return }
switch sender.state {
case .Began:
interactor.hasStarted = true
self.presentViewController(Mview, animated: true, completion: nil)
case .Changed:
interactor.shouldFinish = progress > percentThreshold
interactor.updateInteractiveTransition(progress)
case .Cancelled:
interactor.hasStarted = false
interactor.cancelInteractiveTransition()
case .Ended:
interactor.hasStarted = false
if !interactor.shouldFinish {
interactor.cancelInteractiveTransition()
} else {
interactor.finishInteractiveTransition()
} default:
break
}
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimator()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PresentAnimator()
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
}
class PresentAnimator: NSObject {
}
extension PresentAnimator: UIViewControllerAnimatedTransitioning
{
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC2 = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC2 = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView2 = transitionContext.containerView() else {return}
let initialFrame = transitionContext.initialFrameForViewController(fromVC2)
toVC2.view.frame = initialFrame
toVC2.view.frame.origin.y = -initialFrame.height * 2
containerView2.addSubview(fromVC2.view)
containerView2.addSubview(toVC2.view)
let screenbounds = UIScreen.mainScreen().bounds
let Stage = CGPoint(x: 0, y: 0)
let finalFrame = CGRect(origin: Stage, size: screenbounds.size)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
toVC2.view.frame = finalFrame
}, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
class ModalViewController: UIViewController {
let interactors = Interactor()
var interactor:Interactor? = nil
#IBAction func close(sender: UIButton) {
dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func handleGesture(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
// convert y-position to downward pull progress (percentage)
let translation = sender.translationInView(self.view)
let verticalMovement = translation.y / -view.bounds.height * 2
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else { return }
switch sender.state {
case .Began:
interactor.hasStarted = true
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
interactor.shouldFinish = progress > percentThreshold
interactor.updateInteractiveTransition(progress)
case .Cancelled:
interactor.hasStarted = false
interactor.cancelInteractiveTransition()
case .Ended:
interactor.hasStarted = false
if !interactor.shouldFinish {
interactor.cancelInteractiveTransition()
} else {
interactor.finishInteractiveTransition()
} default:
break
}
}
}
import UIKit
class DismissAnimator: NSObject {
}
extension DismissAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView = transitionContext.containerView()
else {
return
}
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
let screenBounds = UIScreen.mainScreen().bounds
let topLeftCorner = CGPoint(x: 0, y: -screenBounds.height * 2)
let finalFrame = CGRect(origin: topLeftCorner, size: screenBounds.size)
UIView.animateWithDuration(
transitionDuration(transitionContext),animations: {fromVC.view.frame = finalFrame},
completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
If you want a simple Pan Gesture to switch between UIViewControllers, you can check out this:
http://www.appcoda.com/custom-segue-animations/
If you want it to be interactive, as in you can go back and forth between VCs without having to complete the whole transition, I suggest you check out this:
https://www.youtube.com/watch?v=3jAlg5BnYUU
If you want to go even further and have a custom dismissing animation, then look no further than this:
https://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions