Popover notification view, independent of current view controller in swift - ios

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! :)

Related

Conditional change between Child Views in PageView

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.

How do I hide UIView and make it appear when an area is tapped on screen in iOS

I'm building a Video player. I added a controlsContainer UIView (that contains custom playback controls) on top of a UIView I used for my AVPlayerLayer. How do I make this controlsContainer view to always hide after few seconds of appearing and only reappear when an area of the AVPlayerLayer is tapped just like the YouTube iOS App's video player?
Add touch events to AVplayerLayer and when the user touches the player unhide it for say 5 secs and after 5 secs hide it.If you want you can add the animation for hiding and unhiding.
or
If you are using avplayerviewcontroller directly you can simply say
showPlayBackControls=true;
Assuming your container view is on top of your video layer add a tap gesture recognizer to your view and keep track of whether are not your controls are showing. When the state changes animate the change. Here I just set all the subviews alpha to do a fade in/out but you can just as easily set the transform to have a slide in out instead. When the control appears on screen you set a count down timer and have the timer turn the controls off. You could also keep a reference to this timer and then cancel it and reschedule it each time the user interacts with the ui, which is probably better than what I have here, but you get the idea.
class ViewController: UIViewController {
weak var controlView: UIView!
var isHidingControls = false
override func viewDidLoad() {
super.viewDidLoad()
controlView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapControls)))
}
func tapControls() {
isHidingControls = !isHidingControls
animateControls()
}
func animateControls() {
if isHidingControls {
UIView.animate(withDuration: 0.25, animations: {
self.controlView.subviews.forEach {$0.alpha = 0}
}, completion: { _ in
self.controlView.subviews.forEach {$0.isHidden = true}
})
} else {
self.controlView.subviews.forEach {$0.isHidden = true}
UIView.animate(withDuration: 0.25, animations: {
self.controlView.subviews.forEach {$0.alpha = 1}
}, completion: { _ in
Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [weak self] _ in
guard self?.isHidingControls == false else {
return
}
self?.isHidingControls = true
self?.animateControls()
}
})
}
}
}

UIKeyboardWillShowNotification and UIAlert

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.

UIView.transitionWithView breaks the loader's layout

I am trying to animate the root-view-controller-change in my app. After I swap the view controllers, I load the data necessary for the 2nd controller right away. While the data is loading, I show a loader(MBProgressHUD). This is my function for swapping the view controllers:
class ViewUtils {
class func animateRootViewController(duration: NSTimeInterval, changeToViewController: UIViewController) {
let window = UIApplication.sharedApplication().delegate?.window?
if window == nil {
return
}
UIView.transitionWithView(window!,
duration: duration,
options: UIViewAnimationOptions.TransitionFlipFromLeft | UIViewAnimationOptions.AllowAnimatedContent,
animations: {
window!.rootViewController = changeToViewController
},
completion: nil
)
}
}
All good with this but one thing - it totally breaks the loader. I am attaching an imagine of what's happening:
This is the 2nd view controller while rotating. Once the rotation is complete, the loader appears just fine, both the spinner and the text tween to the correct position in the rounded rectangle.
I really don't understand why this happens, would somebody explain it to me, please? Is there a way to prevent it?
The code of the 2nd view controller where I show the loader:
override func viewDidLoad() {
super.viewDidLoad()
hud = HUD(containingView: view)
hud.show()
createBackground()
}
And my hud class:
class HUD {
private var hudBG: UIView!
private var view: UIView!
private(set) var isShown = false
init(containingView: UIView) {
view = containingView
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show() {
if !isShown {
if(hudBG == nil) {
hudBG = UIView(frame: CGRectMake(0, 0, view.bounds.width, view.bounds.height))
hudBG.backgroundColor = UIColor(white: 0, alpha: 0.4)
}
view.addSubview(hudBG)
let hud = MBProgressHUD.showHUDAddedTo(view, animated: true)
hud.mode = MBProgressHUDModeIndeterminate
hud.labelText = "Cargando"
hudBG.alpha = 0
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.hudBG.alpha = 1
})
isShown = true
}
}
func hide() {
if isShown {
UIView.animateWithDuration(0.3, animations: {
() -> Void in
self.hudBG.alpha = 0
}, completion: {
(b) -> Void in
self.hudBG.removeFromSuperview()
})
MBProgressHUD.hideHUDForView(view, animated: true)
isShown = false
}
}
}
Thanks a lot for any ideas!
You are adding the hud to a view that is not properly initialized yet.
If you are loading the view controller from a xib or storyboard, the view and it's subviews have the size as they were loaded from interface.
You have to add the hud after the views have been resized to their final size.
If you move
hud = HUD(containingView: view)
hud.show()
to viewDidLayoutSubviews, it should work fine.
I noticed a similar problem when moving an app from iOS 7 to iOS 8. During animations, especially when scaling was involved, the view positions got distorted.
I am pretty sure it's a bug. The simplest workaround is to animate only screenshots or view snapshots, not actual views - it's more work and you can't have views animating when the main animation is in progress but in general it's a more stable solution.

Custom Input View in Swift

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:

Resources