Lottie with user interaction on iOS - ios

In my iOS app I have lottie animation, which is on the whole screen. During animation user interactions are not available. I want to make everything under animation active to user interaction (scroll, buttons, etc.)
Is there any chance to do it?
CODE:
guard let mainView = self?.view, let animationView = LOTAnimationView(name: "someName") else { return }
animationView.frame = CGRect(x: 0,
y: 0,
width: mainView.frame.size.width,
height: mainView.frame.size.height)
animationView.contentMode = .scaleAspectFill
animationView.loopAnimation = false
mainView.addSubview(animationView)
animationView.play() { _ in
animationView.removeFromSuperview()
}

I bet disabling user interaction for the animation view would do the trick. Try something like:
animationView.userInteractionEnabled = false. This should prevent touches from being consumed by the animationView and not being propagated down the view hierarchy.

Did you tried adding gesture recognizers to Lottie view ?
Edit after code attach :
you can try add gestureRecognizer to animationView and add parameter (sender) on your function.
func handleTap(sender: UITapGestureRecognizer) {
if sender.state == .ended {
// handling code
}
}

Related

How can I move a UIView to another parent while panning?

I'm working on a game prototype in Swift using UIKit and SpriteKit. The inventory (at the bottom of this screenshot) is a UIView with UIImageView subviews for the individual items. In this example, a single acorn.
I have the acorn recognizing the "pan" gesture so I can drag it around. However, it renders it below the other views in the hierarchy. I want it to pop out of the inventory and be on top of everything (even above its parent view) so I can drop it onto other views elsewhere in the game.
This is what I have as my panHandler on the acorn view:
#objc func panHandler(gesture: UIPanGestureRecognizer){
switch (gesture.state) {
case .began:
removeFromSuperview()
controller.view.addSubview(self)
case .changed:
let translation = gesture.translation(in: controller.view)
if let v = gesture.view {
v.center = CGPoint(x: v.center.x + translation.x, y: v.center.y + translation.y)
}
gesture.setTranslation(CGPoint.zero, in: controller.view)
default:
return
}
}
The problem is in the .began case, when I remove it from the superview, the pan gesture immediately cancels. Is it possible to remove the view from a superview and add it as a subview elsewhere while maintaining the pan gesture?
Or, if my approach is completely wrong, could you give me pointers how to accomplish my goal with another method?
The small answer is you can keep the gesture working if you don't call removeFromSuperview() on your view and add it as a subview right away to your controller view, but that's not the right way to do this because if the user cancels the drag you will have to re add to your main view again and if that view your dragging is heavy somehow it gets laggy and messy quickly
The long answer, and in my opinion is the right way to do it and what apple actually does in all drag and drop apis is
you can actually make a snapshot of the view you want to drag and add it as a subview of the controller view that's holding all your views and then call bringSubviewToFront(_ view: UIView) to make sure it's the top most view in the hierarchy and pass in the snapshot you took of the dragging view
in the .began you can hide the original view and in the .ended you can show it again
and also on .ended you can either take that snapshot and add to the dropping view or do anything else with it's dropping coordinates
I made a sample project to apply this
Here is the storyboard design and view hierarchy
Here is the ViewController code
class ViewController: UIViewController {
#IBOutlet var topView: UIView!
#IBOutlet var bottomView: UIView!
#IBOutlet var smallView: UIView!
var snapshotView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let pan = UIPanGestureRecognizer(target: self, action: #selector(panning(_:)))
smallView.addGestureRecognizer(pan)
}
#objc private func panning(_ pan: UIPanGestureRecognizer) {
switch pan.state {
case .began:
smallView.backgroundColor = .systemYellow
snapshotView = smallView.snapshotView(afterScreenUpdates: true)
smallView.backgroundColor = .white
if let snapshotView = self.snapshotView {
self.snapshotView = snapshotView
view.addSubview(snapshotView)
view.bringSubviewToFront(snapshotView)
snapshotView.backgroundColor = .blue
snapshotView.center = bottomView.convert(smallView.center, to: view)
}
case .changed:
guard let snapshotView = snapshotView else {
fallthrough
}
smallView.alpha = 0
let translation = pan.translation(in: view)
snapshotView.center = CGPoint(x: snapshotView.center.x + translation.x, y: snapshotView.center.y + translation.y)
pan.setTranslation(.zero, in: view)
case .ended:
if let snapshotView = snapshotView {
let frame = view.convert(snapshotView.frame, to: topView)
if topView.frame.contains(frame) {
topView.addSubview(snapshotView)
snapshotView.frame = frame
smallView.alpha = 1
} else {
bottomView.addSubview(snapshotView)
let newFrame = view.convert(snapshotView.frame, to: bottomView)
snapshotView.frame = newFrame
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
snapshotView.frame = self.smallView.frame
} completion: { _ in
self.snapshotView?.removeFromSuperview()
self.snapshotView = nil
self.smallView.alpha = 1
}
}
}
default: break
}
}
}
Here is how it ended up

Putting loading animation over VNDocumentViewController Swift

Is it possible to put a loading animation over the VNDocumentViewController? As in, when the user presses the Save button, is there a way for me to somehow indicate that the Vision is processing the image and hasn't frozen? Right now, in my app, there is a long pause between the user pressing Save and the actual image being processed.Here is an example from another post of what I'm trying to create
Here is one example of adding a loading indicator using UIActivityIndicatorView().
startAnimating() to start the animation and stopAnimation() to stop the animation.
iOS - Display a progress indicator at the center of the screen rather than the view
guard let topWindow = UIApplication.shared.windows.last else {return}
let overlayView = UIView(frame: topWindow.bounds)
overlayView.backgroundColor = UIColor.clear
topWindow.addSubview(overlayView)
let hudView = UIActivityIndicatorView()
hudView.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
overlayView.addSubview(hudView)
hudView.center = overlayView.center
hudView.startAnimating()
Alternatively, you could look into using Cocoapod MBProgressHud
https://cocoapods.org/pods/MBProgressHUD
There's a way you can extend a class in Swift that captures this problem well. The idea is you want a UIActivityIndicator in your VNDocumentCameraViewController. But we'd like that to be a part of every version of this we use. We could simply embed the DocumentVC's view into our current view and superimpose a UIActivityIndicator above it in the view stack, but that's pretty hacky. Here's a quick way we can extend any class and solve this problem
import VisionKit
import UIKit
extension VNDocumentCameraViewController {
private struct LoadingContainer {
static var loadingIndicator = UIActivityIndicatorView()
}
var loadingIndicator: UIActivityIndicatorView {
return LoadingContainer.loadingIndicator
}
func animateLoadingIndicator() {
if loadingIndicator.superview == nil {
view.addSubview(loadingIndicator)
//Setup your constraints through your favorite method
//This constrains it to the very center of the controller
loadingIndicator.frame = CGRect(
x: view.frame.width / 2.0,
y: view.frame.height / 2.0,
width: 20,
height: 20)
//Setup additional state like color/etc here
loadingIndicator.color = .white
}
loadingIndicator.startAnimating()
}
func stopAnimatingLoadingIndicator() {
loadingIndicator.stopAnimating()
}
}
The place we can call these functions are in the delegate methods for VNDocumentCameraViewController that you implement in your presenting ViewController:
func documentCameraViewController(
_ controller: VNDocumentCameraViewController,
didFinishWith scan: VNDocumentCameraScan
) {
controller.animateLoadingIndicator()
}

Adding a UIView/UIGestureRecognizer to the presented view in a UIPresentationController

I am trying to recreate the bottom drawer functionality seen in Maps or Siri Shortcuts by using a UIPresentationController by having it recognise user input and updating the frameOfPresentedViewInContainerView accordingly. However I want this mechanism to work independently of the presented UIViewController as much as possible so I'm trying to have the presentation controller add a handle area above the view. Ideally the view of the presented controller and the handle are should both recognise user input.
This works for the presented view, however any view I add to it responds to no UIGestureRecognizer at all. Am I missing something?
class PresentationController: UIPresentationController {
private let handleArea: UIView = UIView()
override var frameOfPresentedViewInContainerView: CGRect {
// Return some frame for now
return CGRect(x: 0, y: 250, width: containerView!.frame.width, height: 500)
}
override func presentationTransitionWillBegin() {
// Unwrap presented view
guard let presentedView = self.presentedView else {
return
}
// Set color
self.handleArea.backgroundColor = UIColor.green
// Add to view hierachy
presentedView.addSubview(self.handleArea)
// Set constraints
self.handleArea.trailingAnchor.constraint(equalTo: presentedView.trailingAnchor).isActive = true
self.handleArea.leadingAnchor.constraint(equalTo: presentedView.leadingAnchor).isActive = true
self.handleArea.bottomAnchor.constraint(equalTo: presentedView.topAnchor).isActive = true
self.handleArea.heightAnchor.constraint(equalToConstant: 56).isActive = true
self.handleArea.translatesAutoresizingMaskIntoConstraints = false
// These don't help
self.handleArea.isUserInteractionEnabled = true
presentedView.isUserInteractionEnabled = true
presentedView.bringSubviewToFront(self.handleArea)
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if completed {
// Add gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onHandleAreaTapped(sender:)))
self.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// Remove subview
self.handleArea.removeFromSuperview()
}
// MARK: - Responder
#objc private func onHandleAreaTapped(sender: UITapGestureRecognizer) {
print("tap") // No output
}
}
I managed to solve it by adding both the handle area and the view of the presentedViewController to a custom view and then overriding the presentedView property and returning my custom view.

How to drag a button from a popover across whole app? (swift)

In my app, I am displaying a popover that shows a custom UIView allowing the user to select a color using various sliders. I also want to implement an 'eyedropper' tool that the user can tap and hold then drag around to choose a color from anything visible in the app. Inside my custom UIView I added a UIPanGestureRecognizer to my button that points to the handlePan method:
var eyedropperStartLocation = CGPoint.zero
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// self is a custom UIView that contains my color selection
// sliders and is placed inside a UITableView that's in
// the popover.
let translation = recognizer.translation(in: self)
if let view = recognizer.view {
switch recognizer.state {
case .began:
eyedropperStartLocation = view.center
case .ended, .failed, .cancelled:
view.center = eyedropperStartLocation
return
default: break
}
view.center = CGPoint(x: view.center.x + translation.x,
y: view.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}
}
I can drag the button around and it changes location, however I have two issues:
The eyedropper button isn't always in front of other items, even inside the popover or the custom UIView inside the popover
The eyedropper button disappears when outside the bounds of the popover
How can I get the button to be visible all the time, including outside the popover? I'll want to detect where the user lets go of it within the app so I can determine what color it was on.
I figured out how to do this so I'll answer my own question. Instead of moving around the view/button that's inside the popup, I create a new UIImageView and add it to the application's Window, letting it span the whole application. The original button stays where it is - you could easily change the state on it to make it look different, or hide it if you wanted to.
You could also use Interface Builder to tie to #IBActions, but I just did everything in code. The clickButton method kicks things off but calculating location in the window and putting it on the screen. The handlePan method does the translation and lets you move it around.
All code below is swift 4 and works in XCode 9.4.1 (assuming I didn't introduce any typos):
// Call this from all your init methods so it will always happen
func commonInit() {
let panner = UIPanGestureRecognizer(target: self, action: #selector(handlePan(recognizer:)))
theButton.addGestureRecognizer(panner)
theButton.addTarget(self, action: #selector(clickButton), for: .touchDown)
eyedropperButton.addTarget(self, action: #selector(unclickButton), for: .touchUpInside)
eyedropperButton.addTarget(self, action: #selector(unclickButton), for: .touchUpOutside)
}
var startLocation = CGPoint.zero
lazy var floatingView: UIImageView = {
let view = UIImageView(image: UIImage(named: "imagename"))
view.backgroundColor = UIColor.blue
return view
}()
// When the user clicks button - we create the image and put it on the screen
// this makes the action seem faster vs waiting for the panning action to kick in
#objc func clickButton() {
guard let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window else { return }
// We ask the button what it's bounds are within it's own coordinate system and convert that to the
// Window's coordinate system and set the frame on the floating view. This makes the new view overlap the
// button exactly.
floatingView.frame = theButton.convert(theButton.bounds, to: nil)
window.addSubview(floatingView)
// Save the location so we can translate it as part of the pan actions
startLocation = floatingView.center
}
// This is here to handle the case where the user didn't move enough to kick in the panGestureRecognizer and cancel the action
#objc func unclickButton() {
floatingView.removeFromSuperview()
}
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self)
switch recognizer.state {
case .ended, .failed, .cancelled:
doSomething()
floatingView.removeFromSuperview()
return
default: break
}
// This section is called for any pan state except .ended, .failed and .cancelled
floatingView.center = CGPoint(x: floatingView.center.x + translation.x,
y: floatingView.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}

Set picker values by tapping anywhere outside of a popup view in iOS

Currently have a popup view which sets 3 picker values by tapping on the SET button:
However, I want to remove the SET button altogether, and have the picker values set upon tapping outside of the popup, which in turn hides the popup.
Here is the current code:
// function for selecting picker values
func pickerDidSet() {
let focusPeriodChoice = focusPeriodDataSource[pickerView.selectedRow(inComponent: 0)]
let breakPeriodChoice = breakPeriodDataSource[pickerView.selectedRow(inComponent: 1)]
let repeatCountChoice = repeatCountDataSource[pickerView.selectedRow(inComponent: 2)]
persistPickerChoice(focusPeriodChoice, dataType: .focusPeriod)
persistPickerChoice(breakPeriodChoice, dataType: .breakPeriod)
persistPickerChoice(repeatCountChoice, dataType: .repeatCount)
timerSummaryLabel.text = "\(focusPeriodChoice)m • \(breakPeriodChoice)m • \(repeatCountChoice)x"
UIView.animate(withDuration: 0.2, animations: { self.pickerContainerView.alpha = 0.0 }, completion: { finished in
self.pickerContainerView.isHidden = true
})
}
// Open popup, by tapping gear icon
#IBAction func openSettings(_ sender: Any) {
pickerView.selectRow(pickerChoiceIndex(forDataType: .focusPeriod), inComponent: 0, animated: false)
pickerView.selectRow(pickerChoiceIndex(forDataType: .breakPeriod), inComponent: 1, animated: false)
pickerView.selectRow(pickerChoiceIndex(forDataType: .repeatCount), inComponent: 2, animated: false)
self.pickerContainerView.isHidden = false
UIView.animate(withDuration: 0.2) {
self.pickerContainerView.alpha = 1.0
}
}
// Once pickers have been set, display the summary
private func configureSummaryLabel() {
let focusPeriodChoice = pickerChoice(forDataType: .focusPeriod)
let breakPeriodChoice = pickerChoice(forDataType: .breakPeriod)
let repeatCountChoice = pickerChoice(forDataType: .repeatCount)
timerSummaryLabel.text = "\(focusPeriodChoice)m • \(breakPeriodChoice)m • \(repeatCountChoice)x"
}
// Setting the picker “SET” button
private func addPickerSetButton(atX x: CGFloat, centerY: CGFloat) {
pickerSetButton.frame = CGRect(x: x, y: 0, width: 40, height: 20)
pickerSetButton.center = CGPoint(x: pickerSetButton.center.x, y: centerY)
pickerSetButton.setTitle("SET", for: .normal)
pickerSetButton.setTitleColor(UIColor.white, for: .normal)
pickerSetButton.setTitleColor(UIColor.darkGray, for: .highlighted)
pickerSetButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
pickerSetButton.addTarget(self, action: #selector(pickerDidSet), for: .touchUpInside)
pickerHeaderView.addSubview(pickerSetButton)
}
If the Previous Black View is you default view of ViewController then all you need is to implemented below method.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check that the touched view is your background view
if touches.first?.view == self.view {
// Do What Every You want to do
}
}
Detail
Every ViewController has a default view object. As in your case the black overlay displaying behind your popup seems like the default view of that view controller. If that black overlay is not your default view then create and IBOutlet of that view which is black opacified in color. And then in the above method where you are check that which view is touch check that if touched view is your black view or not.
Suppose you black view's IBOutlet is backgroundView then the above check will be something like this.
if touches.first?.view == self.backgroundView {
//It means you have touched outside the pop and out side the pop there is only your backgroundView.
//Here you should do exactly the same which you were doing when `SET` button was clicked.
}
touchesBegan method didn't work if touched object is a button so as per you logic.
You need to check if the PickerView is visible then disable it instead of firing the other feature of that button.
Example.
Create a boolean variable named isPickerViewVisible in your class and when picker view is going to visible make it true and when picker view is getting hide just make it false. There might be an IBAction for that red button.
#IBAction didTapButton(_ sender: Any){
//Here you need to check if pickerView is open then disable it. I don't know what logic you have implemented to show picker view.
if isPickerViewVisible {
self.pickerDidSet()
}else {
//Here you should do the task that you do on clicking this button.
}
}

Resources