Search code examples
swiftuiviewpopupuikitxib

Can't add a UIView loaded from XIB as a subView of a ViewController view


I want to implement a popup notification into my app when data was being updated successfully or not. To do that I:

  1. created a .xib file: Screenshot

  2. 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)
    }
    
  3. 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


Solution

  • 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:

    XIB for custom UIView XCode iOS Swift

    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:

    Custom UIView from XIB swift iOS

    Update based on artexhibit (OPs) comments

    The custom view can get its frames in 3 ways that I can think of:

    1. Directly from XIB
    2. By providing frame during programmatic initialization
    3. From the frame set when creating the view in storyboard

    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)
    }
    

    From storyboard Custom UIView XIB in storyboard XCode swift iOS

    This gives the following results

    Custom UIView from XIB initialize swift iOS