I want to create a custom alert via code. This code below is working, but I Need help on some questions about passing data and retain cycles
is it right my usage of [weak self] ?
I'd like to avoid Delegate Pattern, so my plan is to pass an action as handler, this should keep Controller clean and make code more reusable. Is mine a proper solution?
In my mind, a view should not be "auto removing" but its parent should remove it, so I'd like to pass a reference to the parent controller in order to comunicate via completion, but it seems to create a retain circle (deinit never called), so I'm doing this:
self?.removeFromSuperview() //works, but not sure it is right //self?.parentController.removeFromParent() //never calls deinit
I had a problem passing closure as parameter inside an init with cgrect as parameter. Is there a proper way other than this solution to handle that?
required init(refersTo: UIViewController, comp: @escaping () -> Void) { myTransmittedCompletion = comp self.parentController = refersTo super.init(frame: CGRect.zero) }
I call my alert this way
@IBAction func centralButton(_ sender: UIButton) { let alert = MyAlertInCode(refersTo: self, comp: testPerCOmpletion) self.view.addSubview(alert) } func testPerCOmpletion() { print("completion") }
my custom class
class MyAlertInCode: UIView {
let myTransmittedCompletion: () -> Void
let parentController: UIViewController
private let myTitleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.text = "very long long long logn title"
v.textAlignment = .center
v.font = UIFont.systemFont(ofSize: 28, weight: .bold)
return v
}()
private let mySubtTitleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.text = "very long long long logn title"
v.textAlignment = .center
v.font = UIFont.systemFont(ofSize: 20, weight: .bold)
return v
}()
private let myButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Button", for: .normal)
v.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
v.setTitleColor(.systemBlue, for: .normal)
return v
}()
//white panel of the alert
private let container: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
v.layer.cornerRadius = 24
v.backgroundColor = .purple
return v
}()
private lazy var stack: UIStackView = {
let v = UIStackView(arrangedSubviews: [myTitleLabel, mySubtTitleLabel, myButton])
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 10
v.distribution = .fillEqually
v.backgroundColor = .green
return v
}()
required init(refersTo: UIViewController, comp: @escaping () -> Void) {
myTransmittedCompletion = comp
self.parentController = refersTo
super.init(frame: CGRect.zero)
myButton.addTarget(self, action: #selector(methodInsideAlertClass), for: .touchUpInside)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(animateOut)))
self.backgroundColor = UIColor.gray.withAlphaComponent(0.6)
#warning("UIScreen.main.bounds //deprecated in the future, at 14-ott-2022")
// self.frame = UIScreen.main.bounds //deprecated in the future, at 14-ott-2022
guard let windowBoundsFromIOS13 = UIApplication.shared.currentUIWindow()?.rootViewController?.view.bounds else {return}
self.frame = windowBoundsFromIOS13
self.addSubview(container)
NSLayoutConstraint.activate([
container.centerYAnchor.constraint(equalTo: self.centerYAnchor),
container.centerXAnchor.constraint(equalTo: self.centerXAnchor),
container.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7),
container.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5),
])
container.addSubview(stack)
NSLayoutConstraint.activate([
stack.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.6),
stack.centerYAnchor.constraint(equalTo: container.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
])
animateIn()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("❌")
}
//MARK: methods
@objc private func methodInsideAlertClass() {
print("methodInsideAlertClass tapped")
}
@objc private func animateOut() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseIn) {
self.container.transform = CGAffineTransform(translationX: 0, y: -self.frame.height)
self.alpha = 0
} completion: { [weak self] isCompleted in
if isCompleted {
self?.myTransmittedCompletion()
self?.removeFromSuperview() // shouldn't be removed by parent view?
// self?.parentController.removeFromParent() //never calls deinit
}
}
}
@objc private func animateIn() {
self.container.transform = CGAffineTransform(translationX: 0, y: -self.frame.height)
self.alpha = 1
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseIn) {
self.container.transform = .identity
self.alpha = 1
}
}
}
since cannot use Windows:
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}