I want to implement a popup notification into my app when data was being updated successfully or not. To do that I:
created a .xib file: Screenshot
created a class where I load that NIB:
import UIKit
class PopupView: UIView {
static let instance = PopupView()
@IBOutlet weak var backgroundView: UIView!
@IBOutlet weak var popupView: UIVisualEffectView!
@IBOutlet weak var symbol: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
Bundle.main.loadNibNamed("PopupView", owner: self)
popupView.layer.cornerRadius = 20
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showPopup(title: String, message: String, symbol: UIImage, on viewController: UIViewController) {
self.titleLabel.text = title
self.descriptionLabel.text = message
self.symbol.image = symbol
guard let targetView = viewController.view else { return }
backgroundView.frame = targetView.bounds
targetView.addSubview(backgroundView)
}
In the above class I created a showPopup
method where defined a backgroundView
frame to be equal to ViewController bounds.
When I call that method in desired ViewController I receive the behaviour where my popupView
shows itself and then went off the screen straight away (black area in the GIF): GIF
Would you be so kind to help me understand and fix the reason why the popupView
went off the screen and not just equal to a ViewController bounds.
The Code after Shawn Frank's answer
PopupView class:
import UIKit
class PopupView: UIView {
@IBOutlet weak var popupView: UIVisualEffectView!
@IBOutlet weak var symbol: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
private func configure() {
if let views = Bundle.main.loadNibNamed("PopupView", owner: self) {
guard let view = views.first as? UIView else { return }
view.frame = bounds
addSubview(view)
}
}
func showPopup(title: String, message: String, symbol: UIImage, on viewController: UIViewController) {
titleLabel.text = title
descriptionLabel.text = message
self.symbol.image = symbol
popupView.layer.cornerRadius = 20
popupView.clipsToBounds = true
viewController.view.addSubview(self)
UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear) {
self.popupView.center.y += 40
} completion: { _ in
UIView.animate(withDuration: 0.3, delay: 3.0, options: .curveLinear) {
self.popupView.center.y -= 80
} completion: { _ in
self.popupView.removeFromSuperview()
}
}
}
}
Call in desired ViewController:
let width = CGFloat(10)
let midX = (self.view.frame.width - width) / 2.0
let frame = CGRect(x: midX, y: 0, width: width, height: 135)
let popup = PopupView(frame: frame)
popup.showPopup(title: "Test", message: "Tested super test", symbol: UIImage(named: "checkmark.circle.fill")!, on: self)
Constraints in xib: Screenshot
Current result: GIF
I do not use XIB and storyboard too much these days so I also had to refresh my memory and I used this tutorial
This is the custom PopupView class, I prefer not to use singleton for this but it is my personal preference
class PopupView: UIView {
private let xibName = "PopupView"
@IBOutlet weak var visualEffectBackground: UIVisualEffectView!
@IBOutlet weak var titleLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
private func configure() {
if let views = Bundle.main.loadNibNamed(xibName, owner: self),
let popupView = views.first as? UIView {
popupView.frame = bounds
addSubview(popupView)
}
}
func showPopup(title: String,
on viewController: UIViewController) {
guard let targetView = viewController.view else { return }
titleLabel.text = title
layer.cornerRadius = 20
clipsToBounds = true
targetView.addSubview(self)
}
}
XIB is set up like this:
Then in the view controller:
@IBAction func didTapPopUp(_ sender: Any) {
// Give your own width, height etc
let width = CGFloat(180)
let midX = (view.frame.width - width) / 2.0
let frame = CGRect(x: midX, y: 100, width: width, height: 80)
let popup = PopupView(frame: frame)
popup.showPopup(title: "Hello", on: self)
}
Gives me this result:
Update based on artexhibit (OPs) comments
The custom view can get its frames in 3 ways that I can think of:
To make the view work for all these scenarios, we should not do any frame adjustment inside the custom view and leave it to the parent / container / superview
So I made the following changes to work for all scenarios:
class PopupView: UIView {
private static let xibName = "PopupView"
@IBOutlet weak var visualEffectBackground: UIVisualEffectView!
@IBOutlet weak var titleLabel: UILabel!
init() {
super.init(frame: .zero)
configure()
}
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
private func initializeWithNib() {
var popupView = UIView()
if let views = Bundle.main.loadNibNamed(PopupView.xibName,
owner: self),
let view = views.first as? UIView {
popupView = view
}
frame = popupView.bounds
addSubview(popupView)
}
private func initializeWithFrame() {
if let views = Bundle.main.loadNibNamed(PopupView.xibName,
owner: self),
let popupView = views.first as? UIView {
popupView.frame = bounds
addSubview(popupView)
}
}
private func configure() {
if frame == .zero {
initializeWithNib()
}
else {
initializeWithFrame()
}
layer.cornerRadius = 20
clipsToBounds = true
}
func showPopup(title: String,
on viewController: UIViewController) {
guard let targetView = viewController.view else { return }
print(frame)
titleLabel.text = title
targetView.addSubview(self)
}
}
Now this has the flexibility to work with all 3 scenarios:
// In code
@IBAction func didTapPopUp(_ sender: Any) {
// Default initializer
let popup = PopupView()
var originX = (view.frame.width - popup.frame.width) / 2.0
popup.frame.origin = CGPoint(x: originX, y: 100)
popup.showPopup(title: "Hello", on: self)
// Frame initializer
let popupWidth: CGFloat = 200
let popupHeight: CGFloat = 100
originX = (view.frame.width - popupWidth) / 2.0
let originY = (view.frame.height - popupHeight) / 2.0
let popupFrame = CGRect(x: originX,
y: originY,
width: popupWidth,
height: popupHeight)
let popupTwo = PopupView(frame: popupFrame)
popupTwo.showPopup(title: "Frames", on: self)
}
This gives the following results