Libra Studio Log

開発に関することやゲーム、ガジェットなどについてつらつらと書き記しています

StoryboardとUIViewControllerを使ったポップアップビュー

f:id:daihase:20190809100049p:plain

こんばんわ、daihaseです。

ツール系のアプリ開発をしていて、必ずといって言いほど使われるポップアップビュー。何かボタンを押下した時に画面に「にゅ」っと出てきたりする小窓みたいなやつのことですね。

それの実装方法を紹介。いくつかあるのですがここではStoryboardとUIViewControllerを組み合わせた方法を。

StoryboardとUIViewControllerで作った画面を呼び出すためのUIWindowをExtensionしたクラスを作成します。

import UIKit

fileprivate var windowStackArray: [UIWindow] = []
fileprivate var animationDuration: TimeInterval = 0.4

extension UIWindow {
    
    public static var windowStack: [UIWindow] {
        return windowStackArray
    }
    
    open static func createNewWindow(_ rootViewController: UIViewController) -> UIWindow {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.alpha = 0.0;
        window.windowLevel = UIWindowLevelNormal;
        window.rootViewController = rootViewController
        
        return window
    }
    
    open func open(_ animation: ((_ window: UIWindow) -> Void)? = nil) {
        guard let root = rootViewController else {
            return
        }
        
        self.alpha = 0.0
        root.view.frame.origin = CGPoint(x: 0.0, y: 0.0)
        addSubview(root.view)
        
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        
        if windowStackArray.count == 0 {
            windowStackArray.append(appDelegate.window!)
        }
        
        let nowWindow = appDelegate.window
        nowWindow?.rootViewController?.viewWillDisappear(true)
        
        windowStackArray.append(self)
        appDelegate.window = self
        self.makeKeyAndVisible()
        
        if let animation = animation {
            animation(self)
        } else {
            UIView.transition(with: self, duration: animationDuration, options: [.transitionCrossDissolve, .curveLinear], animations:
                { () -> Void in
                    self.alpha = 1.0
                    self.transform = CGAffineTransform.identity
                    
            }, completion: { (finished) in
                nowWindow?.rootViewController?.viewDidDisappear(true)
            })
        }
    }
    
    open func close(_ animation: ((_ window: UIWindow) -> Void)? = nil, completion: ((Bool) -> Swift.Void)? = nil) {
        guard let idx = windowStackArray.index(of: self) else {
            return
        }
        
        if windowStackArray.count <= 1 {
            return
        }
        
        let beforeWindow = windowStackArray[idx - 1]
        
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        
        windowStackArray.remove(at: idx)
        
        if beforeWindow == windowStackArray.last {
            appDelegate.window = beforeWindow
            beforeWindow.rootViewController?.viewWillAppear(true)
        }
        
        if let animation = animation {
            animation(beforeWindow)
        } else {
            UIView.transition(with: self, duration: animationDuration, options: [.transitionCrossDissolve, .curveLinear], animations:
                { () -> Void in
                    self.alpha = 0.0
                    self.transform = CGAffineTransform.identity
            }, completion: { (finished) in
                self.rootViewController?.view.removeFromSuperview()
                self.removeFromSuperview()
                if beforeWindow == windowStackArray.last {
                    beforeWindow.makeKeyAndVisible()
                    beforeWindow.rootViewController?.viewDidAppear(true)
                }
                completion?(finished)
            })
        }
    }
}

 

最後に、作ったポップアップを実際呼び出すための処理をViewContoller側に書きます。

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func tapPopupButton(_ sender: Any) {
        let storyboard = UIStoryboard(name: "PopupViewController", bundle: nil)
        let popupViewController = storyboard.instantiateViewController(withIdentifier: "PopupViewController") as! PopupViewController
        popupViewController.delegate = self
        
        // ポップアップ表示
        UIWindow.createNewWindow(popupViewController).open()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

extension ViewController: PopupViewControllerDelegate {
    // ポップアップ閉じる
    func closeDialog() {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let window = appDelegate.window else {
            return
        }
        
        window.close()
    }
}

 

ポップアップビューの閉じる処理はこの呼び出し側のViewControllerで行うのでdelegateにselfをセットするのを忘れずに。

 

次にポップアップビュー側の処理を。

import UIKit

protocol PopupViewControllerDelegate {
    func closeDialog()
}

class PopupViewController: UIViewController {
    var delegate: PopupViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    @IBAction func tapCloseButton(_ sender: Any) {
        self.delegate?.closeDialog()
    }
}

 

こちらはシンプルですね。protocolでこのポップアップビューを閉じるメソッドを定義し、実際それを実行するのはデリゲート先ということになります。つまり上のViewContollerのcloseDialog()ですね。

 

ちなみにこちらのUIWindow+Extended.swiftを少し弄れば、ポップアップビューが表示される際のアニメーションも自由自在です。

ソースはGitHub上にも置いておきましたのでご自由にいじっちゃってください。

なお、自作のOSSではxibを使ったカスタムUIViewにてポップアップを再現してますので、良かったらこちらを使ってもらえると嬉しかったりします。

それでは良い開発ライフを〜