UI Implementation - Popup
Tags: Message, Popup, iOS, Swift
Published: 2020-04-24 17:52:17

Popup is not the best choice to notify users. It takes the users’ attention, and need action to remove popup. Sometimes, popup is a good selection. It displays on the same view, not change eyesight.

Today, I will implement a popup, inspired by the design of [Joseph Liu](https://dribbble.com/shots/2391749-Referral-Window). Change something to make it simpler.

This is how it looks after coding.

![Final work](https://firebasestorage.googleapis.com/v0/b/blogs-1de93.appspot.com/o/assets%2F2018-10%2F2018-10-09-popup.gif?alt=media&token=53559318-3d67-47f0-a794-1f0bdc13b37d)

# Prepare project
- Create new `Single View App` project
- Add new file `ReferralPopup.swift`

# Define components

```
class ReferralPopup: knView {
    let blackView: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        return button
    }()
    let container: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .white
        v.createRoundCorner(7)
        return v
    }()
    let okButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        let color = UIColor.color(r: 241, g: 66, b: 70)
        button.backgroundColor = color
        button.setTitle("COPY & CONTINUE", for: .normal)
        button.backgroundColor = UIColor.color(r: 71, g: 204, b: 54)
        button.height(54)
        button.createRoundCorner(27)
        return button
    }()
}
```

- `blackView` is the transparent background cover the whole screen. We will dismiss the popup when it clicked. You can use UIView and add UIGestureRecognizer also.

- `container` is the white background container. It’s the main view for popup.

# Layout the container

The most important part is how to layout the container

```
override func setupView() {
    let color = UIColor.color(r: 241, g: 66, b: 70)

    let instruction = "GET YOUR FREE $10 CREDITS"
    let label = UIMaker.makeLabel(text: instruction,
                                  font: UIFont.boldSystemFont(ofSize: 15),
                                  color: .white,
                                  alignment: .center)

    let circle = UIMaker.makeView(background: color)
    let logo = UIMaker.makeImageView(image: UIImage(named: "swift"), contentMode: .scaleAspectFit)
    logo.backgroundColor = .white

    let codeTitle = UIMaker.makeLabel(text: "REFERRAL CODE",
                                      font: UIFont.boldSystemFont(ofSize: 12),
                                      color: UIColor.color(r: 155, g: 165, b: 172),
                                      alignment: .center)
    let codeLabel = UIMaker.makeLabel(text: "KYNGUYEN",
                                      font: UIFont.boldSystemFont(ofSize: 40),
                                      color: UIColor.color(r: 242, g: 64, b: 65),
                                      alignment: .center)


    container.addSubviews(views: circle, label, okButton, logo, codeTitle, codeLabel)

    // (1)
    label.top(toView: container, space: 24)
    label.horizontal(toView: container, space: 24)

    // (2)
    let edge: CGFloat = UIScreen.main.bounds.width * 2
    circle.square(edge: edge)
    circle.createRoundCorner(edge / 2)
    circle.centerX(toView: container)
    circle.bottom(toAnchor: logo.centerYAnchor)

    // (3)
    let logoEdge: CGFloat = 66
    logo.square(edge: logoEdge)
    logo.centerX(toView: circle)
    logo.verticalSpacing(toView: label, space: 40)
    logo.createRoundCorner(logoEdge / 2)
    logo.createBorder(1, color: color)

    // (4)
    codeTitle.centerX(toView: container)
    codeTitle.verticalSpacing(toView: logo, space: 24)

    // (5)
    codeLabel.centerX(toView: container)
    codeLabel.verticalSpacing(toView: codeTitle, space: 8)

    // (6)
    okButton.verticalSpacing(toView: codeLabel, space: 24)
    okButton.bottom(toView: container, space: -24)
    okButton.horizontal(toView: container, space: 36)
    okButton.createRoundCorner(28)

    okButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
    blackView.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
}
```

The sketch is like this

![](https://firebasestorage.googleapis.com/v0/b/blogs-1de93.appspot.com/o/assets%2F2018-10%2F09-popup-frame_explaination.png?alt=media&token=e03f6b90-c6de-4dc0-a78b-2faf586ade15)
## (1)
- `label` is the instruction label, created by my helper `UIMaker`. It is sticked to the `container's topAnchor` with 24px spacing. It should be sticked to `left` and `right`, and `textAlignment` is center to prevent long text can break the UI.

#### Notes

- `top`: layout the `view1's topAnchor` to the `view2's topAnchor`.
- `horizontal`: layout the `view1's leftAnchor` to `view2's leftAnchor` and `view1's rightAnchor` to `view2's rightAnchor`.

## (2)
- `circle` is the top curve. You can use an image instead. The circle has `width` and `height` equal to `UIScreen.main.bounds.width * 2`. It is bigger than the view so we can put its lower edge overlay on the `container`.

- Layout `circle` horizonal center to the view, so the circle looks balance. You can try align to left or right to see differences.

- The `circle`’s bottom edge will align to the `logo` center. A little bit difficult to understand. The `logo` is sticked to the top, and `circle` is sticked to the `logo`. I set the `circle` sticked to the `container`’s top, but the view can’t auto layout. This way, `container` can easy auto layout, we don’t need to care the `container`’s height.

#### Notes
- `square(value: CGFloat)`: set the `view's width` equals to `view's height` and equal to `value`
- `centerX`: stick `view1's centerXAnchor` to `view2's centerXAnchor`
- `bottom`: layout `view1's bottomAnchor` to `view2's bottomAnchor`.

## (3)
- `logo` has `verticalSpacing` to `label` and has 40px spacing. It means the `logo`’s top and the `label`’s bottom has 40px spacing.

#### Notes
- `verticalSpacing`: layout `view1's topAnchor` to `view2's bottomAnchor`

## (4), (5)
- Same to `codeTitle` and `codeLabel`, `verticalSpacing` will make a space between them.

## (6)
- `okButton` has `verticalSpacing` and `bottom`. So keep in mind, the view can automatically calculate the height of the view.

# Display Popup 
The popup is ready to display. Let’s display it.

```
func show(in view: UIView) {
    addSubviews(views: blackView, container)
    blackView.fill(toView: self)
    container.centerY(toView: self)
    container.horizontal(toView: self, space: 24)

    blackView.alpha = 0
    UIView.animate(withDuration: 0.1, animations:
        { [weak self] in self?.blackView.alpha = 1 })
    container.zoomIn(true)

    view.addSubviews(views: self)
    fill(toView: view)
}
```

- We will display the popup inside the controller’s view. Rememeber 3 views: `blackView`, `container`, `self`. We will add `blackView` and `container` to `self`. And we add `self` to `containerView`.

- `view.zoomIn(true)` will show the popup with a slight zoom animation.

# Dismiss popup 

Quite hard here for dismiss animation. The popup zooms in a little bit and zooms out to disappear.

```
@objc func dismiss() {
    let initialValue: CGFloat = 1
    let middleValue: CGFloat = 1.025
    let endValue: CGFloat = 0.001
    func fadeOutContainer() {
        UIView.animate(withDuration: 0.2, animations:
            { [weak self] in self?.container.alpha = 0 })
    }
    func zoomInContainer() {
        UIView.animate(withDuration: 0.05,
                       animations: { [weak self] in self?.container.scale(value: middleValue) })
    }
    func zoomOutContainer() {
        UIView.animate(withDuration: 0.3, delay: 0.05, options: .curveEaseIn,
                       animations:
            { [weak self] in
                self?.container.scale(value: endValue)
                self?.blackView.alpha = 0
            }, completion: { [weak self] _ in self?.removeFromSuperview() })
    }

    container.transform = container.transform.scaledBy(x: initialValue , y: initialValue)
    fadeOutContainer()
    zoomInContainer()
    zoomOutContainer()
}
```

# Time to run
- Add a button to `ViewController` and show the popup in the button event

```
@objc func showReferralPopup() {
    ReferralPopup().show(in: view)
}
```

# Do it better
- You can add new class `Popup` and move components, full `show` function, full `dismiss` function and empty `setupView` to `Popup`.
- The `ReferralPopup` is inherited from `Popup`. You can add new popup easily just set inherit from `Popup` and layout your new view in its `setupView`.
- Full code can pull from [my Github](https://github.com/nguyentruongky/knPopup)