I'm using UIKeyboardWillShowNotification to scroll the view up and down when the keyboard is called. This works fine for the most part. However, the keyboard has a done button which can produce a UIAlert. Without the UIAlert there is no problem, but if the UIAlert is called something strange happens to the scrollview, it seems to stop working do to the size of it becoming smaller.
this is the code I'm using:
func adjustInsetForKeyboardShow(show: Bool, notification: NSNotification) {
guard let value = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue else { return }
let keyboardFrame = value.CGRectValue()
let adjustmentHeight = (CGRectGetHeight(keyboardFrame) + 70) * (show ? 1 : -1)
scrollView.contentInset.bottom += adjustmentHeight
//scrollView.scrollIndicatorInsets.bottom += adjustmentHeight
}
func keyboardWillShow(notification: NSNotification) {
if keyboardVisible == false {
adjustInsetForKeyboardShow(true, notification: notification)
keyboardVisible = true
}
}
func keyboardWillHide(notification: NSNotification) {
adjustInsetForKeyboardShow(false, notification: notification)
keyboardVisible = false
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
the keyboard then has a button that has the following code:
func displayAlert(title:String, message:String, view:UIViewController){
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (action) -> Void in
}))
view.presentViewController(alert, animated: true, completion: nil)
}
The result is that the alert is given, then when I press the OK button the scrollview breaks.
Can anyone help? Let me know if you need more code
I would firstly recommend you to use table view instead of scroll view if possible. Second, I don't know if you tested but these notifications get called more than once and they don't behave as you expect sometimes. I haven't tried it but I assume that once you display UIAlert one of these methods is triggered and then your content size goes crazy. Try setting breakpoints and see what goes on. Also, try dismissing keyboard on return and then call displayAlert(). Also, from experience this deinit method where you remove observers does not get called when you go from the screen, I don't know if you have a reason for using it or? It's better do use viewWillAppear, viewWillDissapear methods.
Related
I have a PageViewController, P, that contains two child ViewControllers, A and B. Both A and B allow a user to enter some data into a form. If the user begins editing the form, I keep track in a boolean variable:
var formEdited = false;
In the event that the user would like to move away from the form, and formEdited is true, I'd like to warn them and say "Are you sure you want to abandon the changes you have in the form?". In the event that they are sure, I'd like to store their data. Otherwise, I'd let them discard the data and move on with their swiping.
As a result, I tried doing something like this in both A and B:
override func viewWillDisappear(_ animated: Bool) {
if (formEdited) {
let dialogMessage = UIAlertController(title: "Confirm", message: "Are you sure you want to abandon the changes you have in the form?", preferredStyle: .alert);
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
super.viewWillDisappear(animated);
})
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { (action) -> Void in
// TODO:: what to do here
self.myCustomFuctionToStoreData();
super.viewWillAppear(true);
}
dialogMessage.addAction(ok);
dialogMessage.addAction(cancel);
self.present(dialogMessage, animated: true, completion: nil);
}
}
As a result, I can see my popup when I try to swipe away from the View. If I click "Cancel", the view remains. ( Which is what I want ) However, if I retry to swipe again, I no longer see the alert box, and the UI changes. ( Which is not what I want. I want it to re-prompt )
I believe that my code needs to react more appropriately when a viewWillDisappear. I think I need to somehow prevent the view from disappearing after this line above:
// TODO:: what to do here
Note: I've tried answers from a few other posts, like this: How do I Disable the swipe gesture of UIPageViewController? , Disable swipe gesture in UIPageViewController , or even these two : Disable UIPageViewController Swipe - Swift and Checking if a UIViewController is about to get Popped from a navigation stack? .
The last two might be most appropriate, but I don't want to disable any gestures nor do i see how i can inject a prevention. I simply want to make the swiping away from a child view a conditional function. How would I do this from my child view ( child of PageView ) in Swift 4 ?
It turns out that implementing conditional scroll operations in a UIPageView is trivial. These are the steps I've taken to solve the problem. ( Updates to this code to make it more efficient are encouraged )
For starters, your UIPageViewController must not be the dataSource. This means that during your scroll operations in child view controllers, nothing will register. ( Which is ok for now ) Instead, you'd want to implement logic for which view is shown when as functions that can be called by the children. These two methods can be added to your UIPageView :
func goToNextView(currentViewController : UIViewController) {
var movingIdx = 0;
let viewControllerIndex = orderedViewControllers.index(of: currentViewController) ?? 0;
if (viewControllerIndex + 1 <= (orderedViewControllers.count - 1)) {
movingIdx = viewControllerIndex + 1;
}
self.setViewControllers([orderedViewControllers[movingIdx]], direction: .forward, animated: true, completion: nil);
}
func goToPreviousView(currentViewController : UIViewController) {
var movingIdx = 0;
let viewControllerIndex = orderedViewControllers.index(of: currentViewController) ?? -1;
if (viewControllerIndex == -1) {
movingIdx = 0;
} else if (viewControllerIndex - 1 >= 0) {
movingIdx = viewControllerIndex - 1;
} else {
movingIdx = orderedViewControllers.count - 1;
}
self.setViewControllers([orderedViewControllers[movingIdx]], direction: .reverse, animated: true, completion: nil);
}
Notes:
It would make sense to update the lines containing ?? 0; to a way to trow an error, or show some default screen.
orderedViewControllers is a list of all child views that this UIPageView controller contains
These methods will be called from child views, so keeping them at this layer makes them very reusable
Lastly, in your child views, you'll need a way to recognize gestures and react on gestures:
override func viewDidLoad() {
super.viewDidLoad();
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(handleGesture))
swipeLeft.direction = .left
self.view.addGestureRecognizer(swipeLeft)
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(handleGesture))
swipeRight.direction = .right
self.view.addGestureRecognizer(swipeRight)
}
And handle the gesture:
#objc func handleGesture(gesture: UISwipeGestureRecognizer) -> Void {
if gesture.direction == UISwipeGestureRecognizerDirection.left {
parentPageViewController.goToNextView(currentViewController: self);
}
else if gesture.direction == UISwipeGestureRecognizerDirection.right {
parentPageViewController.goToPreviousView(currentViewController: self);
}
}
Notes:
In handleGesture function, you'd add your conditional checks to determine if goToNextView or goToPreviousView is ok.
I am trying to move my view up only if certain textField is selected. I got it working, however, if I now select other textField, it activates again on other textFields also, why?
Like this I am dealing with the moving:
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y == 0{
self.view.frame.origin.y -= keyboardSize.height
}
}
}
func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != 0{
self.view.frame.origin.y += keyboardSize.height
}
}
}
And like this I am trying to addObserver in textField touchDown:
#IBAction func didTapCertainTextField(_ sender: Any) {
NotificationCenter.default.addObserver(self, selector: #selector(CreateCardViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
}
Flow in words:
Lets say I have 5 textFields(1, 2, 3, 4, 5)
I add the observer to third textField. I click first, view doesn't move, I click third it moves, I click 1 again and now it moves. Why? I do not want the view to move if clicked textField is 1.
What you are looking for is to know if the textField is first responder, but what you are doing is getting notified when the keyboard is displayed.
I encourage you to look into this class of cocoa:
https://developer.apple.com/reference/uikit/uiresponder
Or you can in the precise cas of you UITextField bind actions to EditingDidBegin and EditingDidEnd through you the Xcode interface builder
or do it programmatically with a delegate
https://developer.apple.com/reference/uikit/uitextfielddelegate
Also I can notice that you start observing this events but it seems to me that you never stop observing them.
For that you should call removeObserver:name:object: at some point before the end of your object's life time
If I understand you correctly, you're trying to move the view up only when the keyboard is hiding the UITextField. If that is the case, I wrote the following extension specifically for that:
extension UIViewController {
func pushViewForTextFields() {
// Push view according text field's position
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillShow(_ sender: Notification) {
var currentEditingTextField: UITextField!
let keyboardHeight = UIScreen.main.bounds.maxY - ((sender as NSNotification).userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
// Find which text field is currently editing (O(n))
for view in self.view.subviews as [UIView] {
if let tf = view as? UITextField {
if tf.isEditing {
currentEditingTextField = tf
break
}
}
}
if currentEditingTextField == nil || currentEditingTextField < 0 {
// no textfield found
return
}
// get absolute frame of the text field, regardless of its parent view
let globalFrame = (currentEditingTextField.superview?.convert(senderObject.frame, to: self.view))!
// the 30 below is completely arbitrary; change it to whatever you want to suit your needs. This is basically the distance you want between the top of the keyboard and the editing text field (30px)
if globalFrame.maxY + 30 > keyboardHeight {
self.view.frame.origin.y = -(globalFrame.maxY + 30 - keyboardHeight)
}
else { // keyboard not covering the text field
self.view.frame.origin.y = 0
}
}
#objc func keyboardWillHide(_ sender: Notification) {
// move view back to normal position when editing is done
self.view.frame.origin.y = 0
}
}
Then simply use it in your UIViewController like this
override func viewDidLoad() {
super.viewDidLoad()
self.pushViewForTextFields()
}
Note that this extension only works if your UITextField objects are on the top level of the main UIView. i.e if they're not embedded in any subViews. If you have any text field in a subview, then you'll have to expand the for-loop by nesting another for-loop in there to check for every view in every subview (O(n^2) in this case).
This may not be the most efficient way (with looping over the UIView's tree and all), but I only wrote it to have a generic solution that I can easily apply to all my UIViewControllers
I'm working on an ios swift app and I'm introducing handling of shake gestures in my app. I want to handle shaking all around the app besides one panel. On this specific panel I want to invoke a completely different function. So first I wrote an extension to UIViewController so that the shake gesture is supported everywhere else:
extension UIViewController:MotionDelegate {
override public func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) {
if motion == .MotionShake {
print("I'm in extension")
self.showAlertMessage("", message: "I'm in extension") //this invokes an alert message
}
}
}
and then in the panel that I want to handle it differently I wrote:
override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) {
if motion == .MotionShake {
showAlertMsg("Yes!", message: "This is some random msg")
}
}
Now the problem is as follows:
when I shake the phone on the screen that I want to behave differently - I see the alert msg with message this is some random msg. But when this screen appears on the screen (and it does for a couple seconds) and I shake the phone again - suddenly it shows the popup I'm in extension. I want to avoid showing the 2nd popup. The problem is that it does not recognize my overrided method when there is a popup up front with a first message. In other cases (when there is no popup) everything works fine. This is my showAlertMsg function:
func showAlertMsg(title: String, message: String, delay: Double){
let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
self.presentViewController(alertController, animated: true, completion: nil)
let delay = delay * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
alertController.dismissViewControllerAnimated(true, completion: nil)
})
}
Is there anything I could do to avoid showing the popup from extension while displaying the popup from the main controller?
Because you have overridden in panel an extension for UIViewController.
Then you present UIAlertController, which has not overridden method.
What you may do, is to make an extension for UIAlertController and handle this function.
I want to notify users that an action has been completed in the background. Currently, the AppDelegate receives notification of this:
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// do something
}
}
I'd really like to display a pop over notification (e.g. "Awarded points to 10 students") over the currently active view.
I know how to do this with NSNotification, but that means I have to add a listener to each of the views. An alternative to that would be great!
The next part of question is how do I actually get the view to fade in and then fade out again in front of whatever view I have - be that a table view, collection view or whatever else. I've tried the following code (in the viewDidLoad for the sake of testing):
override func viewDidLoad() {
// set up views
let frame = CGRectMake(0, 200, 320, 200)
let notificationView = UIView(frame: frame)
notificationView.backgroundColor = UIColor.blackColor()
let label = UILabel()
label.text = "Hello World"
label.tintColor = UIColor.whiteColor()
// add the label to the notification
notificationView.addSubview(label)
// add the notification to the main view
self.view.addSubview(notificationView)
print("Notification should be showing")
// animate out again
UIView.animateWithDuration(5) { () -> Void in
notificationView.hidden = true
print("Notification should be hidden")
}
}
The view does appear without the hiding animation, but with that code in it hides straight away. I'm also not sure how to stick this to the bottom of the view, although perhaps that's better saved for another question. I assume I'm doing a few things wrong here, so any advice pointing me in the right direction would be great! Thanks!
For your notification issue, maybe UIAlertController suits your needs?
This would also solve your issues with fading in/out a UIView
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// Creates an UIAlertController ready for presentation
let alert = UIAlertController(title: "Score!", message: "Awarded points to 10 students", preferredStyle: UIAlertControllerStyle.Alert)
// Adds the ability to close the alert using the dismissViewControllerAnimated
alert.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.Cancel, handler: { action in alert.dismissViewControllerAnimated(true, completion: nil)}))
// Presents the alert on top of the current rootViewController
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
UIAlertController
When adding a subview you want to be on top of everything else, do this:
self.view.addSubview(notificationView)
self.view.bringSubviewToFront(notificationView)
Fading a UIView by changing the alpha directly:
For testing, you should be calling this in your viewDidAppear so that the fading animation starts after the view actually is shown.
// Hides the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
// Displays the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
This solution takes up unnecessary space in your code, I would recommend extensions for this purpose.
Extensions:
Create a Extensions.swift file and place the following code in it.
Usage: myView.fadeIn(), myView.fadeOut()
import UIKit
extension UIView {
// Sets the alpha to 0 over a time period of 0.15 seconds
func fadeOut(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 0
})
}
// Sets the alpha to 1 over a time period of 0.15 seconds
func fadeIn(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 1
})
}
}
Swift 2.1 Extensions
Hope this helps! :)
I've spent hours trying to figure out how to create/then get a custom inputView to work.
I have a grid of TextInputs (think scrabble board) that when pressed should load a custom inputView to insert text.
I've created a .xib file containing the UI elements for the custom inputView. I was able to create a CustomInputViewController and have the inputView appear but never able to get the actual TextInput to update it's value/text.
Apple documentation has seemed light on how to get this to work and the many tutorials I've have seen have been using Obj-C (which I have been unable to convert over due to small things that seem unable to now be done in swift).
What is the overarching architecture and necessary pieces of code that should be implemented to create a customInputView for multiple textInputs (delegate chain, controller, .xibs, views etc)?
Set up a nib file with the appropriate inputView layout and items. In my case I set each button to an action on File Owner of inputViewButtonPressed.
Set up a storyboard (or nib if you prefer) for a view controller.
Then using the following code, you should get what you're looking for:
class ViewController: UIViewController, UITextFieldDelegate {
var myInputView : UIView!
var activeTextField : UITextField?
override func viewDidLoad() {
super.viewDidLoad()
// load input view from nib
if let objects = NSBundle.mainBundle().loadNibNamed("InputView", owner: self, options: nil) {
myInputView = objects[0] as UIView
}
// Set up all the text fields with us as the delegate and
// using our input view
for view in self.view.subviews {
if let textField = view as? UITextField {
textField.inputView = myInputView
textField.delegate = self
}
}
}
func textFieldDidBeginEditing(textField: UITextField) {
activeTextField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
activeTextField = nil
}
#IBAction func inputViewButtonPressed(button:UIButton) {
// Update the text field with the text from the button pressed
activeTextField?.text = button.titleLabel?.text
// Close the text field
activeTextField?.resignFirstResponder()
}
}
Alternatively, if you're wanting to be more keyboard-like, you can use this function as your action (it uses the new let syntax from Swift 1.2), break it up if you need 1.1 compatibility:
#IBAction func insertButtonText(button:UIButton) {
if let textField = activeTextField, title = button.titleLabel?.text, range = textField.selectedTextRange {
// Update the text field with the text from the button pressed
textField.replaceRange(range, withText: title)
}
}
This uses the UITextInput protocol to update the text field as appropriate. Handling delete is a little more complicated, but still not too bad:
#IBAction func deleteText(button:UIButton) {
if let textField = activeTextField, range = textField.selectedTextRange {
if range.empty {
// If the selection is empty, delete the character behind the cursor
let start = textField.positionFromPosition(range.start, inDirection: .Left, offset: 1)
let deleteRange = textField.textRangeFromPosition(start, toPosition: range.end)
textField.replaceRange(deleteRange, withText: "")
}
else {
// If the selection isn't empty, delete the chars in the selection
textField.replaceRange(range, withText: "")
}
}
}
You shouldn't go through all that hassle. There's a new class in iOS 8 called: UIAlertController where you can add TextFields for the user to input data. You can style it as an Alert or an ActionSheet.
Example:
let alertAnswer = UIAlertController(title: "Input your scrabble Answer", message: nil, preferredStyle: .Alert) // or .ActionSheet
Now that you have the controller, add fields to it:
alertAnswer.addTextFieldWithConfigurationHandler { (get) -> Void in
getAnswer.placeholder = "Answer"
getAnswer.keyboardType = .Default
getAnswer.clearsOnBeginEditing = true
getAnswer.borderStyle = .RoundedRect
} // add as many fields as you want with custom tweaks
Add action buttons:
let submitAnswer = UIAlertAction(title: "Submit", style: .Default, handler: nil)
let cancel = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alertAnswer.addAction(submitAnswer)
alertAnswer.addAction(cancel)
Present the controller whenever you want with:
self.presentViewController(alertAnswer, animated: true, completion: nil)
As you see, you have various handlers to pass custom code at any step.
As example, this is how it would look: