こんなやつ
通常のローカル通知ではなく、オリジナルなデザインのローカル通知を出したい!!
ステップ
- バナーを表示する
- バナーを隠す
- ジェスチャーを追加する
- タイマーをセットする
- 使いやすいように修正
ソースだけ見たい人はこちら
1. バナーを表示する
下準備
今回の主役InAppNotificationView
を用意する
class InAppNotificationView: UIView {
// 1
lazy var containerView: UIView = {
let view = UIView()
view.alpha = 0
return view
}()
// 2
lazy var messageLabel: UILabel = {
let label = UILabel()
label.text = "This is In App Notification!!"
label.textAlignment = .center
label.textColor = .white
label.font = .boldSystemFont(ofSize: 20)
label.backgroundColor = .systemBlue
label.layer.cornerRadius = 10
label.clipsToBounds = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// 3
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(containerView)
containerView.addSubview(messageLabel)
let constraints = [
messageLabel.topAnchor.constraint(equalTo: containerView.topAnchor),
messageLabel.leftAnchor.constraint(equalTo: containerView.leftAnchor),
messageLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
messageLabel.rightAnchor.constraint(equalTo: containerView.rightAnchor)
]
NSLayoutConstraint.activate(constraints)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
- 表示したいviewを入れるコンテナviewを用意する。
alpha=0
で透明にしておく - 今回は簡単なメッセージを表示するだけなので、コンテナの中身は単純な
UILabel
- 表示したいviewをコンテナに入れ、コンテナを自身に
addSubview()
で追加する。この時、中身のレイアウトは設定するが、コンテナのレイアウト(frame)は決めない
バナー表示
バナーを表示するメソッドを用意する。基本的に自身のframeは一度決めたら変更はせず、その上にのっけたコンテナviewのframeを操作してバナーの出し入れを表現することに注意。
var targetWindow: UIWindow? {
UIApplication.shared.connectedScenes
.first { $0.activationState == .foregroundActive }
.map { $0 as? UIWindowScene }
.flatMap { $0 }?
.windows.first
}
func showBanner() {
// 1
guard let window = targetWindow else { return }
// 2
let width = window.frame.width
let height = bannerHeight + window.safeAreaInsets.top
frame = CGRect(x: 0, y: 0, width: width, height: height)
// 3
containerView.frame = CGRect(
x: bannerMargin + window.safeAreaInsets.left,
y: height - bannerHeight,
width: width - ((bannerMargin * 2) + window.safeAreaInsets.left + window.safeAreaInsets.right),
height: bannerHeight
)
containerView.setNeedsLayout()
containerView.layoutIfNeeded()
// 4
targetWindow?.addSubview(self)
containerView.alpha = 0
containerView.transform = CGAffineTransform(translationX: 0, y: -frame.height)
// 5
UIView.animate(withDuration: 0.5, animations: {
self.containerView.alpha = 1
self.containerView.transform = .identity
}, completion: nil)
}
- バナーを乗っけるviewを取得する。
UIWindow
を取得するのは、バナーを画面の一番上から表示させるため。言い換えると、statusBarの上から重なる様に表示させるため - 自身(InAppNotificationView)のframeを設定する
- コンテナのframeを設定する。frameの変更を即座に反映するために
setNeedsLayout()
とlayoutIfNeeded()
を実行する - windowに自身を
addSubview()
する。この時alpha=0
で透明にすると共に、コンテナviewを画面の外に隠れる様にtransformしておく -
alpha=1
で透明にしていたコンテナを表示させつつ、transform = .identity
で(4)で行った変形(コンテナの移動)を元に戻す処理をアニメーションさせながら実行する
ここまでを確認する場合
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// view controllerが表示されたら処理スタート!
InAppNotificationView().showBanner()
}
}
バナーが表示されればOK!!
2. バナーを隠す
(1)とは逆の処理
func closeBanner() {
UIView.animate(withDuration: 0.5, animations: {
self.containerView.alpha = 0
self.containerView.transform = .init(translationX: 0, y: -self.frame.height)
}, completion: { _ in
print("on Closed!!")
self.removeFromSuperview()
})
}
バナーを表示させた時とは反対に、またコンテナを透明にしつつ、画面の上に隠す様にアニメーションさせる。完了したら、自身を親viewから削除する
基本的にInAppNotificationViewは画面上部に出しっぱなしで、その上にのせたコンテナviewを表示したり隠したりするイメージ。
3. ジェスチャーを追加する
バナーをタップした時
lazy var tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapped))
@objc func didTapped(_ sender: UITapGestureRecognizer) {
closeBanner()
print("on Tapped!!")
}
バナーをパンした時
lazy var panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPanned))
var currentPositionY: CGFloat = 0
@objc func didPanned(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
break
case .changed:
// 1
let point = sender.translation(in: self)
// 2
guard currentPositionY + point.y <= 0 else { return }
// 3
currentPositionY += point.y
containerView.transform = .init(translationX: 0, y: currentPositionY)
// 4
sender.setTranslation(.zero, in: containerView)
case .ended, .cancelled:
// 5
if (abs(currentPositionY) > bannerHeight / 2) {
closeBanner()
} else {
// 6
UIView.animate(withDuration: 0.2) {
self.containerView.transform = .identity
self.currentPositionY = 0
}
}
default:
break
}
}
panジェスチャーを行っている途中
-
sender.translation(in: self)
で、panされた移動量が取得できる - 下方向へのpanは無視する
- panした移動量の分だけコンテナを変形(移動)させる
- ここでリセットすることによって、(2)でまたpanした移動量を取得できる
panジェスチャーが終わった、又はキャンセルした時
- panジェスチャーによって一定以上、バナーが画面上部に移動していた場合、そのままバナーを閉じる。(
abs()
は数値の絶対値を取得する。) - そうでない時は、バナーを元の位置に戻し、移動量も0にリセットする
4. タイマーをセットする
// 1
var bannerClosingTimer: Timer? {
didSet {
oldValue?.invalidate()
}
}
// 2
func countDownToCloseBanner() {
bannerClosingTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in
self?.closeBanner()
}
}
// 3
func showBanner() {
// ~ ~ ~ ~
countDownToCloseBanner()
}
// 4
case .ended, .cancelled:
if (abs(currentPositionY) > bannerHeight / 2) {
closeBanner()
} else {
// ~ ~ ~ ~
countDownToCloseBanner()
}
// 5
case .began:
bannerClosingTimer = nil
func closeBanner() {
bannerClosingTimer = nil
// ~ ~ ~ ~
}
- Timerを保持するプロパティを宣言。新しいTimerがセットされた時、メモリリークを防ぐために古いTimerを
invalidate()
で無効にする - タイマーをスタートさせるメソッド。今回は5秒経過するとバナーを閉じる
タイマーをスタートするのは
3. バナーを表示させた時
4. 閉じかけたバナーが元の位置に戻った時
タイマーをキャンセルするのは
5. panジェスチャーが開始された時
6. バナーを閉じる時
5. 使いやすいように修正
処理やデータをまとめる
struct InAppNotification {
let message: String
let onTap: (() -> Void)?
let onClosed: (() -> Void)?
}
let notification: InAppNotification
init(notification: InAppNotification) {
self.notification = notification
// ~ ~ ~ ~
}
アプリ内通知に必要なデータ(or処理)をまとめたstructを用意し、InAppNotificationViewに渡す。
あとは適宜、必要なデータ(or処理)を必要な箇所で呼ぶ。
(具体的な箇所は最後に貼ったリンクを参照)
Protocolにまとめる
protocol InAppNotificationShowable {
func showInAppNotification(_ notification: InAppNotification)
}
extension InAppNotificationShowable where Self: UIViewController {
func showInAppNotification(_ notification: InAppNotification) {
InAppNotificationView(notification: notification).showBanner()
}
}
使い方
// InAppNotificationShowableに準拠したUIViewController内で
let notification = InAppNotification(
message: "This is In App Notification!!",
onTap: { print("on tapped!!") },
onClosed: { print("on closed!!") }
)
self.showInAppNotification(notification)
完成!
最後に
コード全てを確認したい方はこちら!!