【Swift】カスタムのToastメッセージをUIViewControllerから呼び出せるようにする
アプリ内でユーザーに状態の変化を通知する際にUIAlertViewなどは少し仰々し過ぎたので、Androidのような簡単なToastメッセージをiOSでも出せないかと思い色々と調べました。
この記事では以下の2パターンのToastメッセージの出し方を書いていきます。
- 文字のみの右上に固定されたToastメッセージ
- LottieAnimationと組み合わせた動きのあるToastメッセージ
1.文字のみのToastメッセージ
前提として、UIViewControllerから呼び出すことを想定するので、UIViewControllerのextensionメソッドとして作っていきます。
コード数も少ないのでいきなり完成したものから出しますが、Toastメッセージを呼び出すメソッドは下記になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
func showToast(message: String, font: UIFont?, type: String){ //ラベルのコンテナViewを作る。 let toastView = ToastMessageShadow(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 35)) switch type { case "alert": toastView.backgroundColor = .red break; case "success": toastView.backgroundColor = .green break; case "notice": toastView.backgroundColor = .blue break; default: toastView.backgroundColor = .blue } //ラベルも作成 let toastLabel = UILabel(frame: CGRect(x: 10, y: 9, width: toastView.bounds.width - 10, height: 17)) if let font = font{ toastLabel.font = font } toastLabel.text = message toastLabel.textColor = .white toastLabel.lineBreakMode = .byTruncatingTail toastLabel.textAlignment = .left //ここでラベルをメッセージの幅に合わせる toastLabel.sizeToFit() //右上の座標を計算する let xPosition = self.view.frame.width - toastLabel.frame.width //ラベルコンテナのFrameを指定し直す。 //@x: = 右から10pt離れるようにする。横幅に+20するから30 //@y: = Status Bar(44pt) + Navigation Bar(97pt) + 10 //@width: = ラベルの横幅 + 20 //@height: = フォントサイズが17でy paddingを9にしてるから 17 + 18 = 35 toastView.frame = CGRect(x: ceil(xPosition) - 30, y: 151.0, width: toastLabel.frame.width + 20, height: 35) //autoresizingMaskでその座標から動かないようにする。念のため toastView.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin] toastView.addSubview(toastLabel) self.view.addSubview(toastView) UIView.animate(withDuration: 1.0, delay: 2.0, options: .curveEaseOut, animations: { toastView.alpha = 0.0 toastView.center.y -= 35 }, completion: { (isCompleted) in toastView.removeFromSuperview() }) } |
それでは読み砕いていきましょう。
手順をまとめると、以下の4つの流れになります。
- ToastメッセージのコンテナとなるUIViewを作成する
- メッセージを挿入するUILabelを作成する
- ラベルの長さなどから最初に作ったコンテナUIViewのFrameを再定義する
- 3秒かけてToastメッセージを削除する
順をおって説明していきます。
1-1.ToastメッセージのコンテナとなるUIViewを作成する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let toastView = ToastMessageShadow(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 35)) switch type { case "alert": toastView.backgroundColor = .red break; case "success": toastView.backgroundColor = .green break; case "notice": toastView.backgroundColor = .blue break; default: toastView.backgroundColor = .blue } |
↑の部分です 。
ToastMessageShadowはUIViewを継承してるカスタムクラスになります。影を入れないと境界線が分かりづらかったので作りました。(コードは追記します。)
toastViewのFrameは後ほど再定義するので、高さ以外は適当に入れています。高さは35ptにしていますが、ここは適宜お好みで問題ないです。
そして、一応通知メッセージをタイプ分け出来るようにしています。タイプがそれぞれ「alert」の場合は背景が赤く、「success」の場合は背景が緑に、そして「notice」の場合は青くなります。
ToastMessageShadowのコードは以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class ToastMessageShadow: UIView { override public func layoutSubviews() { super.layoutSubviews() dropShadow() } private func dropShadow(scale: Bool = true){ self.layer.masksToBounds = false self.layer.cornerRadius = 5 self.layer.shadowColor = UIColor.darkGray.cgColor self.layer.shadowOpacity = 0.5 self.layer.shadowOffset = CGSize(width: 0, height: 1) self.layer.shadowRadius = 3 self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 5).cgPath self.layer.shouldRasterize = true self.layer.rasterizationScale = scale ? UIScreen.main.scale : 1 } } |
1-2.メッセージを挿入するUILabelを作成する
1 2 3 4 5 6 7 8 |
let toastLabel = UILabel(frame: CGRect(x: 10, y: 9, width: toastView.bounds.width - 10, height: 17)) if let font = font{ toastLabel.font = font } toastLabel.text = message toastLabel.textColor = .white toastLabel.lineBreakMode = .byTruncatingTail toastLabel.textAlignment = .left |
↑の部分です。
メッセージを挿入するUILabelを作成します。
このUILabelは先ほど作ったtoastViewの中に挿入するので、Frameの内容はtoastViewと相対的になっています。
toastViewに少しpaddingを入れたいので、UILabelのx座標は左から10ptほど離れるようにし、同じく右からも10ptほど離れるようにしたいので、横幅をtoastViewの横幅 – 10ptにしています。
さらに、font sizeを17ptにするのでUILabelの高さも17ptにします。toastViewの高さは35ptなので、35 – 17で余りが18ptあります。toastViewの中心にUILabelを据えたいのでy座標を18の半分の9ptにします。
これでUILabelのFrameは完成しました。あとはメッセージをUILabelに挿入して、色やalignmentを設定していきます。
1-3.ラベルの長さなどから最初に作ったコンテナUIViewのFrameを再定義する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//ここでラベルをメッセージの幅に合わせる toastLabel.sizeToFit() //右上の座標を計算する let xPosition = self.view.frame.width - toastLabel.frame.width //ラベルコンテナのFrameを指定し直す。 //@x: = 右から10pt離れるようにする。横幅に+20するから30 //@y: = Status Bar(44pt) + Navigation Bar(97pt) + 10 //@width: = ラベルの横幅 + 20 //@height: = フォントサイズが17でy paddingを9にしてるから 17 + 18 = 35 toastView.frame = CGRect(x: ceil(xPosition) - 30, y: 151.0, width: toastLabel.frame.width + 20, height: 35) //autoresizingMaskでその座標から動かないようにする。念のため toastView.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin] toastView.addSubview(toastLabel) self.view.addSubview(toastView) |
↑の部分です。
まず、メッセージの幅にラベルの幅をsizeToFitメソッドで合わせます。そして先ほど適当に作ったtoastViewのFrameを再定義していきます。
この記事では右上に固定したいので、まず右上のx座標を計算していきます。
画面の横幅からラベルの横幅を引き、ラベルのx paddingに20ptほどあるのと画面の右端から10pt離したいので、更に30pt引いていきます。
y座標はステータスバーの44ptとナビゲーションバーの97ptとpadding分の10ptで合計151ptほど離しています。
横幅は先ほどsizeToFitしたUILabelの横幅にx padding分の20ptを足します。
高さは変わらず35ptです。
そして、autoresizingMaskで定義した座標から動かないようにし、完成です。
あとはtoastViewにUILabelをaddSubviewしてからUIViewControllerのViewにaddSubviewします。
1-4.3秒かけてToastメッセージを削除する
1 2 3 4 5 6 7 8 9 |
UIView.animate(withDuration: 1.0, delay: 2.0, options: .curveEaseOut, animations: { toastView.alpha = 0.0 toastView.center.y -= 35 }, completion: { (isCompleted) in toastView.removeFromSuperview() }) |
↑の部分です。
Toastメッセージなので、自動的に消えるようにします。
UIViewには便利なanimateメソッドがあるので、それを使っていきます。
ちゃんとユーザーがメッセージを認識できるように、2秒ほどdelay(遅延)をかけて、その後に1秒間でtoastViewの透明度を0にし、少し上に浮くようにanimationが適用されます。animationが終わったら、toastViewを親のViewから削除します。
以上で、文字のみのToastメッセージ完了です。
使い方はUIViewControllerから以下のように呼び出せます。
1 2 3 4 5 6 7 8 9 |
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let font = UIFont(name: "HiraginoSans-W3", size: 17) self.showToast(message: "トーストメッセージを表示しました", font: font, type: "success") } } |
2.LottieAnimationと組み合わせた動きのあるToastメッセージ
この例では、先ほど紹介した文字のみのToastメッセージの応用でLottie AnimationをtoastViewに追加していきます。
LottieとはAirbnbが作ったアニメーションのRendererで、iOS・Android・Webなどで簡単にアニメーションを再生出来るライブラリを提供しています。
この記事では詳しいことは割愛しますが、気になる方は下記の記事を参考にしてください。
それではいきなり完成したものから出します。Lottie Animation付きのToastメッセージを呼び出すメソッドは下記になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func showToastWithLottie(message: String, font: UIFont?, lottie_image: String){ let toastView = CornerShadowView(frame: CGRect(x: 25, y: self.view.center.y - 150, width: self.view.frame.width - 50, height: self.view.frame.height/3)) toastView.backgroundColor = .white let lottieAnimationView = AnimationView(name: "\(lottie_image)") let edge = self.view.frame.width/4 lottieAnimationView.frame = CGRect(x: (self.view.center.x - (edge/2) - 25) , y: 25, width: edge, height: edge) lottieAnimationView.contentMode = .scaleAspectFit toastView.addSubview(lottieAnimationView) let toastLabel = UILabel(frame: CGRect(x: 25, y: (edge + 25), width: toastView.bounds.width - 50, height: edge)) if let font = font{ toastLabel.font = font } toastLabel.textColor = .darkText toastLabel.numberOfLines = 0 toastLabel.lineBreakMode = .byTruncatingTail let attrString = NSMutableAttributedString(string: message) let style = NSMutableParagraphStyle() style.lineHeightMultiple = 1.7 style.alignment = .center attrString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: message.count)) toastLabel.attributedText = attrString toastView.addSubview(toastLabel) self.view.addSubview(toastView) lottieAnimationView.play() UIView.animate(withDuration: 1.0, delay: 2.0, options: .curveEaseOut, animations: { toastView.alpha = 0.0 }, completion: { (isCompleted) in toastView.removeFromSuperview() }) } |
それでは読み砕いていきましょう。
手順をまとめると、以下の4つの流れになります。
- ToastメッセージのコンテナとなるUIViewを作成する
- アニメーションのコンテナとなるLottieAnimationViewを作成する
- メッセージを挿入するUILabelを作成する
- 3秒かけてToastメッセージを削除する
順をおって説明していきます。
2-1.ToastメッセージのコンテナとなるUIViewを作成する
1 2 |
let toastView = CornerShadowView(frame: CGRect(x: 25, y: self.view.center.y - 150, width: self.view.frame.width - 50, height: self.view.frame.height/3)) toastView.backgroundColor = .white |
↑の部分です。
CornerShadowViewもまた、UIViewクラスを継承しているカスタムクラスになります。こちらのコードも後ほど追記します。
今回はアニメーションが追加されていることから、Toastメッセージのコンテナはやや大きめに、画面の中心から少し上あたりに配置しようと思います。
画面の横端からそれぞれ25pt離れるようにしたいので、x座標を25pt、横幅を画面の横幅引く25ptにします。
y座標は画面の中心y座標から150pt程上に置きます。
最後にToastメッセージの高さは画面の高さを三分割にした高さにします。
CornerShadowViewのコードは以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class CornerShadowView: UIView { override public func layoutSubviews() { super.layoutSubviews() dropShadow() } private func dropShadow(scale: Bool = true){ self.layer.masksToBounds = false self.layer.cornerRadius = 10 self.layer.shadowColor = UIColor.darkGray.cgColor self.layer.shadowOpacity = 0.7 self.layer.shadowOffset = CGSize(width: 0, height: 3) self.layer.shadowRadius = 9 self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath self.layer.shouldRasterize = true self.layer.rasterizationScale = scale ? UIScreen.main.scale : 1 } } |
2-2.アニメーションのコンテナとなるLottieAnimationViewを作成する
1 2 3 4 5 |
let lottieAnimationView = AnimationView(name: "\(lottie_image)") let edge = self.view.frame.width/4 lottieAnimationView.frame = CGRect(x: (self.view.center.x - (edge/2) - 25) , y: 25, width: edge, height: edge) lottieAnimationView.contentMode = .scaleAspectFit toastView.addSubview(lottieAnimationView) |
↑の部分です。
アニメーション用のViewはLottieライブラリのAnimationViewを使って予めダウンロードしてあるjson fileを呼び出しています。
Lottieのアニメーションファイルは以下のサイトからダウンロード出来ます。
CCライセンスも数多く投稿されていますし、もちろんMarketplaceから購入することも出来ます。
話を戻しまして、lottieAnimationViewはtoastViewの上半分あたりに置き、左右のpaddingは25ptほどにします。
まず、lottieAnimationViewの寸法を画面の横幅の1/4の長さに定義します。
ですので、lottieAnimationViewのx座標は画面の中心x座標からlottieAnimationView自身の辺の長さ(画面横幅の1/4)を更に半分にして、padding分の25ptを引いた値になります。
y座標は適当に25pt上に開けています。
長さと高さは先ほど定義した通りの、画面横幅の1/4の長さです。
あとはaspectFitさせて、toastViewにaddSubviewします。
2-3.メッセージを挿入するUILabelを作成する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let toastLabel = UILabel(frame: CGRect(x: 25, y: (edge + 25), width: toastView.bounds.width - 50, height: edge)) if let font = font{ toastLabel.font = font } toastLabel.textColor = .darkText toastLabel.numberOfLines = 0 toastLabel.lineBreakMode = .byTruncatingTail let attrString = NSMutableAttributedString(string: message) let style = NSMutableParagraphStyle() style.lineHeightMultiple = 1.7 style.alignment = .center attrString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: message.count)) toastLabel.attributedText = attrString toastView.addSubview(toastLabel) self.view.addSubview(toastView) lottieAnimationView.play() |
↑の部分です。
次に、toastViewの下半分のメッセージ部分を作っていきます。
UILabelのx座標はtoastViewの右端から25pt程離します。
y座標はlottieAnimationView分の高さと25pt程更に離してます。
横幅はtoastViewの横幅にpadding分の50ptを引いています。
高さはきっかり計算するのがめんどくさかったので、適当にlottieAnimationViewの高さと同じにしちゃっています。多分少しだけ無駄な空間が出来るかもしれません。
一応、ラベルが2段になっても問題ないようにnumberOfLinesを0にし、行間を1.7ぐらいに設定しています。
その他のラベル設定が完了したら、toastViewにaddSubviewしてからtoastViewをUIViewcontrollerのViewにaddSubviewします。
そしてこのままですとアニメーションが再生されないので、playメソッドからアニメーションを再生します。
4. 3秒かけてToastメッセージを削除する
1 2 3 4 5 6 7 8 |
UIView.animate(withDuration: 1.0, delay: 2.0, options: .curveEaseOut, animations: { toastView.alpha = 0.0 }, completion: { (isCompleted) in toastView.removeFromSuperview() }) |
↑の部分です。
ここは1-4.で紹介した内容と大体同じなので、割愛します。
このメソッドも先ほどと同様、UIViewControllerから以下のように呼び出せます。
1 2 3 4 5 6 7 8 9 |
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let font = UIFont(name: "HiraginoSans-W3", size: 17) self.showToastWithLottie(message: "おめでとうございます!\nトーストメッセージを表示しました", font: font, lottie_image: "star") } } |
以上、カスタムのToastメッセージをUIViewControllerから呼び出す方法でした。
最終的なコードは以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
import Foundation import UIKit import Lottie extension UIViewController { func showToast(message: String, font: UIFont?, type: String){ //ラベルのコンテナViewを作る。 let toastView = ToastMessageShadow(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 35)) switch type { case "alert": toastView.backgroundColor = .red break; case "success": toastView.backgroundColor = .green break; case "notice": toastView.backgroundColor = .blue break; default: toastView.backgroundColor = .blue } //ラベルも作成 let toastLabel = UILabel(frame: CGRect(x: 10, y: 9, width: toastView.bounds.width - 10, height: 17)) if let font = font{ toastLabel.font = font } toastLabel.text = message toastLabel.textColor = .white toastLabel.lineBreakMode = .byTruncatingTail toastLabel.textAlignment = .left //ここでラベルをメッセージの幅に合わせる toastLabel.sizeToFit() //右上の座標を計算する let xPosition = self.view.frame.width - toastLabel.frame.width //ラベルコンテナのFrameを指定し直す。 //@x: = 右から10pt離れるようにする。横幅に+20するから30 //@y: = Status Bar(44pt) + Navigation Bar(97pt) + 10 //@width: = ラベルの横幅 + 20 //@height: = フォントサイズが17でy paddingを9にしてるから 17 + 18 = 35 toastView.frame = CGRect(x: ceil(xPosition) - 30, y: 151.0, width: toastLabel.frame.width + 20, height: 35) //autoresizingMaskでその座標から動かないようにする。念のため toastView.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin] toastView.addSubview(toastLabel) self.view.addSubview(toastView) UIView.animate(withDuration: 1.0, delay: 2.0, options: .curveEaseOut, animations: { toastView.alpha = 0.0 toastView.center.y -= 35 }, completion: { (isCompleted) in toastView.removeFromSuperview() }) } func showToastWithLottie(message: String, font: UIFont?, lottie_image: String){ let toastView = CornerShadowView(frame: CGRect(x: 25, y: self.view.center.y - 150, width: self.view.frame.width - 50, height: self.view.frame.height/3)) toastView.backgroundColor = .white let lottieAnimationView = AnimationView(name: "\(lottie_image)") let edge = self.view.frame.width/4 lottieAnimationView.frame = CGRect(x: (self.view.center.x - (edge/2) - 25) , y: 25, width: edge, height: edge) lottieAnimationView.contentMode = .scaleAspectFit toastView.addSubview(lottieAnimationView) let toastLabel = UILabel(frame: CGRect(x: 25, y: (edge + 25), width: toastView.bounds.width - 50, height: edge)) if let font = font{ toastLabel.font = font } toastLabel.textColor = .darkText toastLabel.numberOfLines = 0 toastLabel.lineBreakMode = .byTruncatingTail let attrString = NSMutableAttributedString(string: message) let style = NSMutableParagraphStyle() style.lineHeightMultiple = 1.7 style.alignment = .center attrString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: message.count)) toastLabel.attributedText = attrString toastView.addSubview(toastLabel) self.view.addSubview(toastView) lottieAnimationView.play() UIView.animate(withDuration: 1.0, delay: 2.0, options: .curveEaseOut, animations: { toastView.alpha = 0.0 }, completion: { (isCompleted) in toastView.removeFromSuperview() }) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import Foundation import UIKit public class CornerShadowView: UIView { override public func layoutSubviews() { super.layoutSubviews() dropShadow() } private func dropShadow(scale: Bool = true){ self.layer.masksToBounds = false self.layer.cornerRadius = 10 self.layer.shadowColor = UIColor.darkGray.cgColor self.layer.shadowOpacity = 0.7 self.layer.shadowOffset = CGSize(width: 0, height: 3) self.layer.shadowRadius = 9 self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath self.layer.shouldRasterize = true self.layer.rasterizationScale = scale ? UIScreen.main.scale : 1 } } public class ToastMessageShadow: UIView { override public func layoutSubviews() { super.layoutSubviews() dropShadow() } private func dropShadow(scale: Bool = true){ self.layer.masksToBounds = false self.layer.cornerRadius = 5 self.layer.shadowColor = UIColor.darkGray.cgColor self.layer.shadowOpacity = 0.5 self.layer.shadowOffset = CGSize(width: 0, height: 1) self.layer.shadowRadius = 3 self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 5).cgPath self.layer.shouldRasterize = true self.layer.rasterizationScale = scale ? UIScreen.main.scale : 1 } } |
クレジット
Lottie Animation: Favourite app icon
by Michael Harvey