Can anyone recommend a library for implementing growl or toast-style notifications on iOS? For example, after a user saves a profile, I wan开发者_C百科t to have a notification fade in, linger for 3 seconds, report "profile saved", and fade out. Right now I have a UIAlertView that interrupts the user's workflow with a single "OK" button, and I feel like that is overkill.
The Android Toast class is an example of what I am looking for on iOS.
Thanks!
I created a solution that I think you'll find useful: https://github.com/scalessec/toast
It's written as a obj-c category, essentially adding makeToast methods to any instance of UIView. eg:
[self.view makeToast:@"Profile saved"
duration:2.0
position:@"bottom"];
I solved it this way:
- Create common label on your view. Make it all screen wide, give it the size you will need and center text in it.
- Set it's position "on top" - this label must be below all of your controls in the list of controls.
- Add it to interface, properties, synthesize (let's call it "toastLabel" there).
- Associate in your XIB file with "toastLabel"
Add following line to your viewWillAppear to hide label for beginning:
[toastLabel setHidden:TRUE];
Add the following code on Button click (or some other event):
toastLabel.text = @"Our toast text"; [toastLabel setHidden:TRUE]; [toastLabel setAlpha:1.0]; CGPoint location; location.x = 160; location.y = 220; toastLabel.center = location; location.x = 160; location.y = 320; [toastLabel setHidden:FALSE]; [UIView animateWithDuration:0.9 animations:^{ toastLabel.alpha = 0.0; toastLabel.center = location; }];
This label will "fall down" and disappear.
Albeit a little late, here's my take on it:
https://github.com/pcperini/PCToastMessage
You could try my open source library TSMessages: https://github.com/toursprung/TSMessages
It's really easy to use and looks beautiful on iOS 5/6 and on iOS 7 as well.
I made my own. The class linked to by Krishnan was ugly and didn't rotate correctly.
https://github.com/esilverberg/ios-toast
Here's what it looks like:
I made it in this way :
+ (void)showToastMessage:(NSString *)message {
UIAlertView *toast = [[UIAlertView alloc] initWithTitle:nil
message:message
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:nil, nil];
[toast show];
// duration in seconds
int duration = 2;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[toast dismissWithClickedButtonIndex:0 animated:YES];
});
}
Updated solution for iOS9+:
+ (void)showToastMessage:(NSString *)message
root:(id)view {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
// duration in seconds
int duration = 2;
[view presentViewController:alertController animated:YES completion:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[alertController dismissViewControllerAnimated:YES completion:nil];
});
}
MBProgressHUD?
Hey you are looking for this.
https://github.com/PaulSolt/WEPopover#readme
This one is exactly what you want. This one is quite handy as well since it has a completion block, please have a look :) https://github.com/PrajeetShrestha/EkToast
Swift 2.0:
The idea is to work out a Toast class with zero dependency on CocoaPods.
Reference: https://github.com/scalessec/Toast-Swift
Make an empty Swift file (file-New-File- Empty Swift File - Name it Toast.)
Add the following code to it.
// Toast.swift
import UIKit
import ObjectiveC
enum ToastPosition {
case Top
case Center
case Bottom
}
extension UIView {
private struct ToastKeys {
static var Timer = "CSToastTimerKey"
static var Duration = "CSToastDurationKey"
static var Position = "CSToastPositionKey"
static var Completion = "CSToastCompletionKey"
static var ActiveToast = "CSToastActiveToastKey"
static var ActivityView = "CSToastActivityViewKey"
static var Queue = "CSToastQueueKey"
}
private class ToastCompletionWrapper {
var completion: ((Bool) -> Void)?
init(_ completion: ((Bool) -> Void)?) {
self.completion = completion
}
}
private enum ToastError: ErrorType {
case InsufficientData
}
private var queue: NSMutableArray {
get {
if let queue = objc_getAssociatedObject(self, &ToastKeys.Queue) as? NSMutableArray {
return queue
} else {
let queue = NSMutableArray()
objc_setAssociatedObject(self, &ToastKeys.Queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return queue
}
}
}
// MARK: - Make Toast Methods
func makeToast(message: String) {
self.makeToast(message, duration: ToastManager.shared.duration, position: ToastManager.shared.position)
}
func makeToast(message: String, duration: NSTimeInterval, position: ToastPosition) {
self.makeToast(message, duration: duration, position: position, style: nil)
}
func makeToast(message: String, duration: NSTimeInterval, position: CGPoint) {
self.makeToast(message, duration: duration, position: position, style: nil)
}
func makeToast(message: String, duration: NSTimeInterval, position: ToastPosition, style: ToastStyle?) {
self.makeToast(message, duration: duration, position: position, title: nil, image: nil, style: style, completion: nil)
}
func makeToast(message: String, duration: NSTimeInterval, position: CGPoint, style: ToastStyle?) {
self.makeToast(message, duration: duration, position: position, title: nil, image: nil, style: style, completion: nil)
}
func makeToast(message: String?, duration: NSTimeInterval, position: ToastPosition, title: String?, image: UIImage?, style: ToastStyle?, completion: ((didTap: Bool) -> Void)?) {
var toastStyle = ToastManager.shared.style
if let style = style {
toastStyle = style
}
do {
let toast = try self.toastViewForMessage(message, title: title, image: image, style: toastStyle)
self.showToast(toast, duration: duration, position: position, completion: completion)
} catch ToastError.InsufficientData {
print("Error: message, title, and image are all nil")
} catch {}
}
func makeToast(message: String?, duration: NSTimeInterval, position: CGPoint, title: String?, image: UIImage?, style: ToastStyle?, completion: ((didTap: Bool) -> Void)?) {
var toastStyle = ToastManager.shared.style
if let style = style {
toastStyle = style
}
do {
let toast = try self.toastViewForMessage(message, title: title, image: image, style: toastStyle)
self.showToast(toast, duration: duration, position: position, completion: completion)
} catch ToastError.InsufficientData {
print("Error: message, title, and image cannot all be nil")
} catch {}
}
// MARK: - Show Toast Methods
func showToast(toast: UIView) {
self.showToast(toast, duration: ToastManager.shared.duration, position: ToastManager.shared.position, completion: nil)
}
func showToast(toast: UIView, duration: NSTimeInterval, position: ToastPosition, completion: ((didTap: Bool) -> Void)?) {
let point = self.centerPointForPosition(position, toast: toast)
self.showToast(toast, duration: duration, position: point, completion: completion)
}
func showToast(toast: UIView, duration: NSTimeInterval, position: CGPoint, completion: ((didTap: Bool) -> Void)?) {
objc_setAssociatedObject(toast, &ToastKeys.Completion, ToastCompletionWrapper(completion), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if let _ = objc_getAssociatedObject(self, &ToastKeys.ActiveToast) as? UIView where ToastManager.shared.queueEnabled {
objc_setAssociatedObject(toast, &ToastKeys.Duration, NSNumber(double: duration), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(toast, &ToastKeys.Position, NSValue(CGPoint: position), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.queue.addObject(toast)
} else {
self.showToast(toast, duration: duration, position: position)
}
}
// MARK: - Activity Methods
func makeToastActivity(position: ToastPosition) {
// sanity
if let _ = objc_getAssociatedObject(self, &ToastKeys.ActiveToast) as? UIView {
return
}
let toast = self.createToastActivityView()
let point = self.centerPointForPosition(position, toast: toast)
self.makeToastActivity(toast, position: point)
}
func makeToastActivity(position: CGPoint) {
// sanity
if let _ = objc_getAssociatedObject(self, &ToastKeys.ActiveToast) as? UIView {
return
}
let toast = self.createToastActivityView()
self.makeToastActivity(toast, position: position)
}
//Dismisses the active toast activity indicator view.
func hideToastActivity() {
if let toast = objc_getAssociatedObject(self, &ToastKeys.ActivityView) as? UIView {
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.CurveEaseIn, .BeginFromCurrentState], animations: { () -> Void in
toast.alpha = 0.0
}, completion: { (finished: Bool) -> Void in
toast.removeFromSuperview()
objc_setAssociatedObject(self, &ToastKeys.ActivityView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
})
}
}
// MARK: - Private Activity Methods
private func makeToastActivity(toast: UIView, position: CGPoint) {
toast.alpha = 0.0
toast.center = position
objc_setAssociatedObject(self, &ToastKeys.ActivityView, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.addSubview(toast)
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: .CurveEaseOut, animations: { () -> Void in
toast.alpha = 1.0
}, completion: nil)
}
private func createToastActivityView() -> UIView {
let style = ToastManager.shared.style
let activityView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: style.activitySize.width, height: style.activitySize.height))
activityView.backgroundColor = style.backgroundColor
activityView.autoresizingMask = [.FlexibleLeftMargin, .FlexibleRightMargin, .FlexibleTopMargin, .FlexibleBottomMargin]
activityView.layer.cornerRadius = style.cornerRadius
if style.displayShadow {
activityView.layer.shadowColor = style.shadowColor.CGColor
activityView.layer.shadowOpacity = style.shadowOpacity
activityView.layer.shadowRadius = style.shadowRadius
activityView.layer.shadowOffset = style.shadowOffset
}
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
activityIndicatorView.center = CGPoint(x: activityView.bounds.size.width / 2.0, y: activityView.bounds.size.height / 2.0)
activityView.addSubview(activityIndicatorView)
activityIndicatorView.startAnimating()
return activityView
}
// MARK: - Private Show/Hide Methods
private func showToast(toast: UIView, duration: NSTimeInterval, position: CGPoint) {
toast.center = position
toast.alpha = 0.0
if ToastManager.shared.tapToDismissEnabled {
let recognizer = UITapGestureRecognizer(target: self, action: "handleToastTapped:")
toast.addGestureRecognizer(recognizer)
toast.userInteractionEnabled = true
toast.exclusiveTouch = true
}
objc_setAssociatedObject(self, &ToastKeys.ActiveToast, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.addSubview(toast)
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.CurveEaseOut, .AllowUserInteraction], animations: { () -> Void in
toast.alpha = 1.0
}) { (Bool finished) -> Void in
let timer = NSTimer(timeInterval: duration, target: self, selector: "toastTimerDidFinish:", userInfo: toast, repeats: false)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
objc_setAssociatedObject(toast, &ToastKeys.Timer, timer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
private func hideToast(toast: UIView) {
self.hideToast(toast, fromTap: false)
}
private func hideToast(toast: UIView, fromTap: Bool) {
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.CurveEaseIn, .BeginFromCurrentState], animations: { () -> Void in
toast.alpha = 0.0
}) { (didFinish: Bool) -> Void in
toast.removeFromSuperview()
objc_setAssociatedObject(self, &ToastKeys.ActiveToast, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if let wrapper = objc_getAssociatedObject(toast, &ToastKeys.Completion) as? ToastCompletionWrapper, completion = wrapper.completion {
completion(fromTap)
}
if let nextToast = self.queue.firstObject as? UIView, duration = objc_getAssociatedObject(nextToast, &ToastKeys.Duration) as? NSNumber, position = objc_getAssociatedObject(nextToast, &ToastKeys.Position) as? NSValue {
self.queue.removeObjectAtIndex(0)
self.showToast(nextToast, duration: duration.doubleValue, position: position.CGPointValue())
}
}
}
// MARK: - Events
func handleToastTapped(recognizer: UITapGestureRecognizer) {
if let toast = recognizer.view, timer = objc_getAssociatedObject(toast, &ToastKeys.Timer) as? NSTimer {
timer.invalidate()
self.hideToast(toast, fromTap: true)
}
}
func toastTimerDidFinish(timer: NSTimer) {
if let toast = timer.userInfo as? UIView {
self.hideToast(toast)
}
}
// MARK: - Toast Construction
func toastViewForMessage(message: String?, title: String?, image: UIImage?, style: ToastStyle) throws -> UIView {
// sanity
if message == nil && title == nil && image == nil {
throw ToastError.InsufficientData
}
var messageLabel: UILabel?
var titleLabel: UILabel?
var imageView: UIImageView?
let wrapperView = UIView()
wrapperView.backgroundColor = style.backgroundColor
wrapperView.autoresizingMask = [.FlexibleLeftMargin, .FlexibleRightMargin, .FlexibleTopMargin, .FlexibleBottomMargin]
wrapperView.layer.cornerRadius = style.cornerRadius
if style.displayShadow {
wrapperView.layer.shadowColor = UIColor.blackColor().CGColor
wrapperView.layer.shadowOpacity = style.shadowOpacity
wrapperView.layer.shadowRadius = style.shadowRadius
wrapperView.layer.shadowOffset = style.shadowOffset
}
if let image = image {
imageView = UIImageView(image: image)
imageView?.contentMode = .ScaleAspectFit
imageView?.frame = CGRect(x: style.horizontalPadding, y: style.verticalPadding, width: style.imageSize.width, height: style.imageSize.height)
}
var imageRect = CGRectZero
if let imageView = imageView {
imageRect.origin.x = style.horizontalPadding
imageRect.origin.y = style.verticalPadding
imageRect.size.width = imageView.bounds.size.width
imageRect.size.height = imageView.bounds.size.height
}
if let title = title {
titleLabel = UILabel()
titleLabel?.numberOfLines = style.titleNumberOfLines
titleLabel?.font = style.titleFont
titleLabel?.textAlignment = style.titleAlignment
titleLabel?.lineBreakMode = .ByTruncatingTail
titleLabel?.textColor = style.titleColor
titleLabel?.backgroundColor = UIColor.clearColor();
titleLabel?.text = title;
let maxTitleSize = CGSize(width: (self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: self.bounds.size.height * style.maxHeightPercentage)
let titleSize = titleLabel?.sizeThatFits(maxTitleSize)
if let titleSize = titleSize {
titleLabel?.frame = CGRect(x: 0.0, y: 0.0, width: titleSize.width, height: titleSize.height)
}
}
if let message = message {
messageLabel = UILabel()
messageLabel?.text = message
messageLabel?.numberOfLines = style.messageNumberOfLines
messageLabel?.font = style.messageFont
messageLabel?.textAlignment = style.messageAlignment
messageLabel?.lineBreakMode = .ByTruncatingTail;
messageLabel?.textColor = style.messageColor
messageLabel?.backgroundColor = UIColor.clearColor()
let maxMessageSize = CGSize(width: (self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: self.bounds.size.height * style.maxHeightPercentage)
let messageSize = messageLabel?.sizeThatFits(maxMessageSize)
if let messageSize = messageSize {
messageLabel?.frame = CGRect(x: 0.0, y: 0.0, width: messageSize.width, height: messageSize.height)
}
}
var titleRect = CGRectZero
if let titleLabel = titleLabel {
titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
titleRect.origin.y = style.verticalPadding
titleRect.size.width = titleLabel.bounds.size.width
titleRect.size.height = titleLabel.bounds.size.height
}
var messageRect = CGRectZero
if let messageLabel = messageLabel {
messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding
messageRect.size.width = messageLabel.bounds.size.width
messageRect.size.height = messageLabel.bounds.size.height
}
let longerWidth = max(titleRect.size.width, messageRect.size.width)
let longerX = max(titleRect.origin.x, messageRect.origin.x)
let wrapperWidth = max((imageRect.size.width + (style.horizontalPadding * 2.0)), (longerX + longerWidth + style.horizontalPadding))
let wrapperHeight = max((messageRect.origin.y + messageRect.size.height + style.verticalPadding), (imageRect.size.height + (style.verticalPadding * 2.0)))
wrapperView.frame = CGRect(x: 0.0, y: 0.0, width: wrapperWidth, height: wrapperHeight)
if let titleLabel = titleLabel {
titleLabel.frame = titleRect
wrapperView.addSubview(titleLabel)
}
if let messageLabel = messageLabel {
messageLabel.frame = messageRect
wrapperView.addSubview(messageLabel)
}
if let imageView = imageView {
wrapperView.addSubview(imageView)
}
return wrapperView
}
// MARK: - Helpers
private func centerPointForPosition(position: ToastPosition, toast: UIView) -> CGPoint {
let padding: CGFloat = ToastManager.shared.style.verticalPadding
switch(position) {
case .Top:
return CGPoint(x: self.bounds.size.width / 2.0, y: (toast.frame.size.height / 2.0) + padding)
case .Center:
return CGPoint(x: self.bounds.size.width / 2.0, y: self.bounds.size.height / 2.0)
case .Bottom:
return CGPoint(x: self.bounds.size.width / 2.0, y: (self.bounds.size.height - (toast.frame.size.height / 2.0)) - padding)
}
}
}
// MARK: - Toast Style
struct ToastStyle {
var backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.8)
var titleColor = UIColor.whiteColor()
var messageColor = UIColor.whiteColor()
var maxWidthPercentage: CGFloat = 0.8 {
didSet {
maxWidthPercentage = max(min(maxWidthPercentage, 1.0), 0.0)
}
}
var maxHeightPercentage: CGFloat = 0.8 {
didSet {
maxHeightPercentage = max(min(maxHeightPercentage, 1.0), 0.0)
}
}
var horizontalPadding: CGFloat = 10.0
var verticalPadding: CGFloat = 10.0
var cornerRadius: CGFloat = 10.0;
var titleFont = UIFont.boldSystemFontOfSize(16.0)
var messageFont = UIFont.systemFontOfSize(16.0)
var titleAlignment = NSTextAlignment.Left
var messageAlignment = NSTextAlignment.Left
var titleNumberOfLines = 0;
var messageNumberOfLines = 0;
var displayShadow = false;
var shadowColor = UIColor.blackColor()
var shadowOpacity: Float = 0.8 {
didSet {
shadowOpacity = max(min(shadowOpacity, 1.0), 0.0)
}
}
var shadowRadius: CGFloat = 6.0
var shadowOffset = CGSize(width: 4.0, height: 4.0)
var imageSize = CGSize(width: 80.0, height: 80.0)
var activitySize = CGSize(width: 100.0, height: 100.0)
var fadeDuration: NSTimeInterval = 0.2
}
// MARK: - Toast Manager
class ToastManager {
static let shared = ToastManager()
var style = ToastStyle()
var tapToDismissEnabled = true
var queueEnabled = true
var duration: NSTimeInterval = 3.0
var position = ToastPosition.Bottom
}
Using Toast.swift:
// basic usage
self.view.makeToast("Sample Toast")
// toast with a specific duration and position
self.view.makeToast("Sample Toast", duration: 3.0, position: .Top)
// toast with all possible options
self.view.makeToast("Sample Toast", duration: 2.0, position: CGPoint(x: 110.0, y: 110.0), title: "Toast Title", image: UIImage(named: "ic_120x120.png"), style:nil) { (didTap: Bool) -> Void in
if didTap {
print("completion from tap")
} else {
print("completion without tap")
}
}
//display toast with an activity spinner
self.view.makeToastActivity(.Center)
// display any view as toast
let sampleView = UIView(frame: CGRectMake(100,100,200,200))
sampleView.backgroundColor = UIColor(patternImage: UIImage(named: "ic_120x120")!)
self.view.showToast(sampleView)
self.view.showToast(sampleView, duration: 3.0, position: .Top, completion: nil)
You can download a sample project from https://github.com/alvinreuben/ToastSample
This is something, which was needed by you. Created with lot of Animation Options, screen positions, and duration. Even you can provide your own duration. Check that out below.
https://github.com/avistyles/AVIToast
Swift 3
Have been using Rannie/Toast-Swift for Swift 3 very happily for a while now and can recommend it for a very similar "Android-like" experience. Its very simple to implement without needing another pod and pretty customizable depending on your needs.
Easy Peasy
view.makeToastActivity()
view.hideToastActivity()
精彩评论