WatchOS warning: rejected resignFirstResponder when being removed from hierarchy - swift

I'm getting this error in Xcode.
2018-02-26 07:13:22.326888-0500 Watch Extension[1298:2691330] [View]
First responder warning: '<SPInterfacePicker: 0x14dc1740; frame = (76
0; 58 44); gestureRecognizers = <NSArray: 0x14dcd8a0>; layer =
<CALayer: 0x14dc1910>>' rejected resignFirstResponder when being
removed from hierarchy
My InterfaceController has 4 WkInterfacePickers and it seems like this error might be related to presenting an alert (when the user saves data), but I am not sure.
Has anyone else ever seen this?
My code:
if successSaving == true {
DispatchQueue.main.async {
WKInterfaceDevice.current().play(.success)
self.showSuccessAlertWith(message: "Workout Saved, Stats Added.")
}
func showSuccessAlertWith(message: String){
let action1 = WKAlertAction(title: "OK", style: .default) {
WKInterfaceController.reloadRootPageControllers(withNames: ["InterfaceController"],
contexts: nil,
orientation: .vertical,
pageIndex: 0)
}
presentAlert(withTitle: "Success", message: message, preferredStyle: .alert, actions: [action1])
}
}

I think what is happening is that WKInterfacePickers are very easy to accidentally leave in a "still editing" state, in other words if you scroll through the values then tap "Done" button (which calls reloadRootPageControllers) the system thinks the user was still in the process of editing the picker's value.
I can just ask users to be more careful (unlikely)...but I am unsure how to solve which in iOS the equivalent would be calling resignFirstResponder.
Neither resignFocus nor setting isActive to false, according to my testing, prevent this message from logging.

Related

AVFoundation Video Camera crashes when returning to view controller Swift 4

I'm creating an app that users AVFoundation to record a video. I have a Login View Contoller which leads to a View Controller (user can see their profile details here) which leads to a Data Collection View Controller (this is where the video camera is presented). All works fine but when I click on a back button to go back to the View Controller, and then click 'start data collection' to go back into the video camera a 2nd time, the app crashes.
Crash info:
2018-08-11 11:39:51.861569+0100 LiopaDatacapture-iOS[6343:1642808] *
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '* -[AVCaptureMetadataOutput
setMetadataObjectTypes:] Unsupported type found - use
-availableMetadataObjectTypes'
*** First throw call stack: (0x1842e6d8c 0x1834a05ec 0x189e54b44 0x10128f4fc 0x1012989a4 0x101298ca4 0x18df01e64 0x18df01a50
0x18eaa4fd8 0x18e12b398 0x18e12a25c 0x18e3a33a0 0x18e0e13e4
0x18e1297bc 0x18e129654 0x18e3a6350 0x18e734d24 0x18e881af4
0x18e8819a0 0x18e39a49c 0x101297f5c 0x18e05564c 0x18e176870
0x18e05b700 0x18e1911a8 0x18e0d89e0 0x18e0cd890 0x18e0cc1d0
0x18e8add1c 0x18e8b02c8 0x18e8a9368 0x18428f404 0x18428ec2c
0x18428c79c 0x1841acda8 0x186192020 0x18e1cc758 0x101284a90
0x183c3dfc0) libc++abi.dylib: terminating with uncaught exception of
type NSException (lldb)
I'm using AVCaptureMetadataOutput to detect faces. This code is in my SessionHandler class where the video camera is being set up.
// define metadata
let metaOutput = AVCaptureMetadataOutput()
if cameraSession.canAddOutput(metaOutput) {
metaOutput.setMetadataObjectsDelegate(self, queue: faceQueue)
cameraSession.addOutput(metaOutput)
print("metaoutput added")
}
// set metadata to look for faces
metaOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.face]
In my DataCaptureViewController.swift file this is where I load the video camera-
override func viewDidLoad() {
super.viewDidLoad()
sessionHandler.setupCamera()
audioRecorder.setUpAudioSession()
createObservers()
let layer = sessionHandler.layer
layer.frame = previewView.bounds
previewView.layer.addSublayer(layer)
view.layoutIfNeeded()
jsonSentence.text = "Press start button to get phrase"
startButton.setTitle("Start", for: .normal)
}
I've played around a bit trying viewDidAppear and viewWillAppear but I'm new to swift and don't think I fully understand what they do or if this is what's causing the problem.
The SessionHandler class variables need to be shared with an Objective-C class so I've created a shared instance of it to be used throughout the app.
static let sharedSession = SessionHandler()
It's hard to know what code is useful to share but if you need any more info I'm happy to provide it.
The answer seemed pretty straightforward in the end, sorry for the big question!
It was because the metadataObjectTypes was empty when the DataCaptureViewController was reloaded. So I created a viewDidAppear method and set the metadataObjectTypes to face in there.
override func viewDidLoad() {
super.viewDidLoad()
sessionHandler.setupCamera()
audioRecorder.setUpAudioSession()
createObservers()
let layer = sessionHandler.layer
layer.frame = previewView.bounds
previewView.layer.addSublayer(layer)
view.layoutIfNeeded()
jsonSentence.text = "Press start button to get phrase"
startButton.setTitle("Start", for: .normal)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
sessionHandler.metaOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.face]
}
The app seems to be working now. If I've done this incorrectly though feel free to correct!

App changes to different controller view when changing orientation

I'm developing an app using Xcode 9.2 and swift 4 and I needed to allow just one view in my app to change to landscape, so I added the code below to my AppDelegate
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if globalVariables.gIsDosageView == "Y" {
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
return UIInterfaceOrientationMask.all;
} else {
return UIInterfaceOrientationMask.portrait;
}
} else {
return UIInterfaceOrientationMask.portrait;
}
}
The global variable is set to "N" on every other controller and so only DosageView has it set to "Y". In the DosageView controller I added this method to help with the transition.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
hideViews()
if size.width < 400 {
DispatchQueue.main.async() {
self.userImg.isHidden = false
self.burgerImg.isHidden = false
self.deviceImg.isHidden = false
self.portraitView.isHidden = false
self.landscapeView.isHidden = true
}
}
else {
DispatchQueue.main.async() {
self.userImg.isHidden = true
self.burgerImg.isHidden = true
self.deviceImg.isHidden = true
self.portraitView.isHidden = true
self.landscapeView.isHidden = false
self.loadLandscapeView()
}
}
}
I've set up a LandscapeView and a PortraitView and the method above helps to toggle between them and it all works fine. If I move to the next view or return to the main screen and then return to the DosageView and then change the orientation to Landscape it works but if I go to the next view and then from there to the Connect view and connect (via BLE, NFC or QR) it then returns to the DosageView. When I change the orientation to Landscape it changes back to the Connect view. There is no direct link between these two views as you have to go through the Instruction view to get to Connect View for the DosageView, so how can this happen?
When I run it under debug and put a breakpoint in the ViewDidLoad on Connect View, it runs every time the DosageView changes to Landscape. If anyone can tell me what is going on I would be grateful as this is driving me crazy.
It was actually a colleague who sorted this for me and as he correctly pointed out it was a Lottie run call causing the problem. For anyone using Lottie and experiencing this problem, simply check your animation code. I had originally plagiarised code from a Splash screen as below
splashAnimation!.loopAnimation = false
splashAnimation!.play(fromProgress: 0,
toProgress: 1.0,
withCompletion: { (finished) in
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "Menu")
self.present(controller, animated: true, completion: nil)
})
This code works fine as it moves on to the main screen on completion. However, the screen that was causing the problems moves between two different screens and to fix the problem, my colleague simply removed the withCompletion part as below
splashAnimation!.loopAnimation = true
splashAnimation!.play(fromProgress: 0,
toProgress: 1.0)

Show please wait overlay in swift asynchronously

I am using the answer located here for creating a "please wait" overlay. And I also used this answer for the asynchronous logic. What I am trying to do is create this overlay asynchronously. Ultimately, what I would like to do is just create a method which shows this overlay, and when that method call returns I want to be sure that the overlay has been fully presented. So something like this:
Helper.showPleaseWaitOverlay()
doSomeOtherTask() // when we get here, overlay should be fully presented
Helper.hidePleaseWaitOverlay()
I realize I could do something like this (e.g. use the completion callback of the present method) :
Helper.showPleaseWaitOverlay() {
doSomeOtherTask()
Helper.hidePleaseWaitOverlay()
}
But I really am just curious as to why the below code doesn't work. What ends up happening is that the group.wait() call just hangs and never returns.
What am I doing wrong?
// create a dispatch group which we'll use to keep track of when the async
// work is finished
let group = DispatchGroup()
group.enter()
// create the controller used to show the "please wait" overlay
var pleaseWaitController = UIAlertController(title: nil, message: "Please Wait...", preferredStyle: .alert)
// present the "please wait" overlay as an async task
DispatchQueue.global(qos: .default).async {
// we must perform the GUI work on main queue
DispatchQueue.main.async {
// create the "please wait" overlay to display
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
self.pleaseWaitController!.view.addSubview(loadingIndicator)
self.present(self.pleaseWaitController!, animated: true) {
// the "please wait" overlay has now been presented, so leave the dispatch group
group.leave()
}
}
}
// wait for "please wait" overlay to be presented
print("waiting for please wait overlay to be presented")
group.wait() // <---- Call just hangs and never completes
print("done waiting for please wait overlay to be presented")

Programmatically disabling screenshot in App

I want to prevent taking screenshot of a page in app.
how to do it programmatically so that screenshots cannot be taken.
Found code to detect screenshot. Can it be deleted as soon as a screenshot is taken?
let mainQueue = NSOperationQueue.mainQueue()
NSNotificationCenter.defaultCenter().addObserverForName(UIApplicationUserDidTakeScreenshotNotification,
object: nil,
queue: mainQueue) { notification in
// executes after screenshot
}
There is no way to prevent ScreenShots but you can prevent Screen Recording
through this code.
func detectScreenRecording(action: #escaping () -> ()) {
let mainQueue = OperationQueue.main
NotificationCenter.default.addObserver(forName: UIScreen.capturedDidChangeNotification, object: nil, queue: mainQueue) { notification in
// executes after screenshot
action()
}
}
//Call in vewWillApper
detectScreenRecording {
print(UIScreen.main.isCaptured)
if UIScreen.main.isCaptured {
//your vier hide code
print("self.toHide()")
} else {
// self.sceneDeleg(ate?.window?.isHidden = false
//your view show code
print("self.toShow()")
}
}
There is absolutely no way to completely prevent user from taking screenshot during the app process, and that's because you do not have access to delete photos in the photo gallery of the user. It would totally be a security issue if you could access your user's photos.
However, there are ways to partially prevent screenshots, as described here: Prevent screen capture in an iOS app
Technically that is possible, via the Photos framework, the docs for which can be found here.
Example code can be found here.
However, this will ask the user's permission first, and then again to confirm deletion; so possibly not the ideal solution. Unfortunately this is as good as it gets as Apple has the Camera Roll fairly locked down.
You cannot prevent user from taking screenshot, however, you can hide the content while a screenshot is taken, Use this code to do so..
extension UIView {
func hideContentOnScreenCapture() {
DispatchQueue.main.async {
let field = UITextField()
field.isSecureTextEntry = true
self.addSubview(field)
field.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
field.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
self.layer.superlayer?.addSublayer(field.layer)
field.layer.sublayers?.first?.addSublayer(self.layer)
}
}
}
Usage:
yourView.hideContentOnScreenCapture()

Resume animateWithDuration .Repeat after return to foreground

Unfortunately, I found out that UIView.animateWithDuration stops once you minimize your app to homescreen and does not resume once you bring it back to the foreground.
I've spent the last hour trying to figure out how to solve this (by detecting background/foreground switching) and resulted to adding an observer. I did it and successfully detected it with a debugging message in console; however, I'm sure of how to resume the animation(s) in my view(s). What is the correct way to pause/resume or even restart the animation when the app is loaded back into the foreground?
ViewController.swift
class ViewController: UIViewController {
func cameBackFromSleep(sender : AnyObject) {
// Restart animation here or resume?
print ("If you can see this, congrats... observer works :-)")
}
override func viewDidLoad() {
super.viewDidLoad()
// Observer to detect return from background
NSNotificationCenter.defaultCenter().addObserver( self, selector: #selector(ViewController0.cameBackFromSleep(_:)), name: UIApplicationDidBecomeActiveNotification, object: nil )
// Add a label
let label = UILabel(frame: CGRectMake(0, 600 , 200, 200 ))
label.text = "test message"
label.font = UIFont.boldSystemFontOfSize(12)
label.sizeToFit()
self.view.addSubview(label)
// Animation to move label
func animateText() {
UIView.animateWithDuration(2.0, delay: 0.0, options: [ .Autoreverse, .Repeat, .CurveEaseInOut, .BeginFromCurrentState], animations: {
label.alpha = 0.3
label.frame.origin.x = ((global.maxwidth/2) * -1)
}, completion: { finished in
if finished {
label.frame.origin.x = 0.0
}
})
}
// This guy here stops once you minimize the app to background
animateText()
}
}
Put animateText() inside comeBackFromSleep(_:). I believe that resuming an animation is kinda hard so just restart it.
The behavior somewhat makes sense because of this (you can take a look yourself at the AppDelegate's methods):
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
It states that you should refresh the UI (which includes animations). So it's normal behavior for the animations to stop after you put an app in the background.