LoginSignup
3
1

More than 3 years have passed since last update.

UIalertControllerが微妙に使いづらかったのでいい感じのAlertControllerを自作する!

Posted at

はじめに

先日インターン先のタスクで共通で使う通知部分を実装する際、デフォルトのUIAlertControllerだとボタンのテキストをいじれなかったりとデザインに合わせることが難しかったので、一からUialertControllerに似た実装の仕方で作ってみたのでそれについてまとめてみようと思います!

github

デザインの用件内容

・アラートのttitleやmessage部分のフォントやtextColorを変えたい
(デフォルトのアラートだと変更不可能)
・アラートの上部にバーナー画像を任意で追加可能にして欲しい
・ボタンの数を任意で選択できるようにしてフォントや文字色も選択可能にして欲しい
・なるべく使う際にUIAlerttControllerに似た使い方ができるようにして欲しい

完成図

①アニメーション部分

はじめに土台となるアラートのアニメーション部分を作成しました!

a
public class DialogAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    // true: dismiss
    // false: present
    private let isPresenting: Bool
    init(isPresenting: Bool) {
        self.isPresenting = isPresenting
    }
    public func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.20
    }
    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if isPresenting {
            dismissAnimation(transitionContext)
        } else {
            presentAnimation(transitionContext)
        }
    }
    private func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
        guard let alert = transitionContext
            .viewController(forKey: UITransitionContextViewControllerKey.to) as? DialogController
            else { fatalError("ViewController is not defined sucessfully") }
        let container = transitionContext.containerView
        alert.backgroundView.alpha = 0
        alert.contentView.alpha = 0
        alert.contentView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
        container.addSubview(alert.view)
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            alert.backgroundView.alpha = 1
            alert.contentView.alpha = 1
            alert.contentView.transform = .identity
        }, completion: { _ in
            transitionContext.completeTransition(true)
        })
    }
//DissMissAnimaitonb部分は省略

ポイントとして
ただ単にViewを表示するだけだと,遷移前の画面が投下されないので表示されるViewをbackgroundViewとAlertViewに分割してbackgroundViewを透過させることで無事表示できるようになりました

②Controller部分について

a
//Controller部分
public class DialogController: UIViewController, UIViewControllerTransitioningDelegate {
    //DialogControllerのベースとなるView
    private(set) lazy var backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.black.withAlphaComponent(0.15)
        view.isOpaque = false
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    let contentView: UIView = .init()
    let contentStackView: UIStackView = .init()

    private let bannerView: UIImageView = .init()
    private var bannerAspectRatioConstraint: NSLayoutConstraint?
    private let textContainer: UIView = UIView()
    private let textStackView: UIStackView = UIStackView()
    private lazy var textStackViewTopAnchor: NSLayoutConstraint = {
        return textStackView.topAnchor.constraint(equalTo: textContainer.topAnchor, constant: 40)
    }()
    private lazy var textStackViewBottomAnchor: NSLayoutConstraint = {
        return textStackView.bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: -40)
    }()
    private let titleLabel: UILabel = .init()
    private let messageLabel: UILabel = .init()
    private let buttonStackview: UIStackView = .init()
    private let topBorder: UIView = .init()
    private var actions = [DialogAction]()

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        providesPresentationContextTransitionStyle = true
        definesPresentationContext = true
        modalPresentationStyle = UIModalPresentationStyle.custom
        transitioningDelegate = self
    }
    //簡易イニシャライザ
    //引数がnilかどうかでcontentViewが可変する
    public convenience init(title: String, message: String?, banner: UIImage?) {
        self.init(nibName: nil, bundle: nil)
        titleLabel.text = title
        messageLabel.isHidden = message == nil
        messageLabel.text = message
        bannerView.image = banner
        bannerView.isHidden = banner == nil
        textStackViewTopAnchor.constant = banner == nil ? 40 : 24
        textStackViewBottomAnchor.constant = banner == nil ? -40 : -24

        if let banner = banner {
            bannerAspectRatioConstraint
                = bannerView.heightAnchor.constraint(equalTo: bannerView.widthAnchor,
                                                     multiplier: banner.size.height / banner.size.width)
        }
    }

    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func viewDidLoad() {
        super.viewDidLoad()
        setAttrributes()
        constructViews()
        setLayout()
    }

    @objc private func buttonEvent(sender: UIButton) {
        self.dismiss(animated: true, completion: nil)
    }

    public func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DialogAnimation(isPresenting: true)
    }
//ボタン追加時のメゾット、二つ目のボタン追加時からボタンの追加前にborderを設置する
    public func add(action: DialogAction) {
        let button = UIButton()
        button.setTitle(action.title, for: UIControl.State())
        button.setTitleColor(action.titleColor, for: .normal)
        button.titleLabel?.font = action.titleFont
        button.heightAnchor.constraint(equalToConstant: 44).isActive = true
        button.addTarget(self, action: #selector(buttonEvent(sender:)), for: UIControl.Event.touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        if actions.count > 0 {
            let border = UIView()
            border.translatesAutoresizingMaskIntoConstraints = false
            border.widthAnchor.constraint(equalToConstant: 0.5).isActive = true
            border.heightAnchor.constraint(equalToConstant: 44).isActive = true
            border.backgroundColor = UIColor.gray
            buttonStackview.addArrangedSubview(border)
        }
        buttonStackview.addArrangedSubview(button)
        if let alertView = buttonStackview.arrangedSubviews.first {
            button.widthAnchor.constraint(equalTo: alertView.widthAnchor ).isActive = true
        }
        actions.append(action)
    }

    public func animationController(
        forPresented _: UIViewController,
        presenting _: UIViewController,
        source _: UIViewController
    ) -> UIViewControllerAnimatedTransitioning? {
        return DialogAnimation(isPresenting: false)
    }
}

Controller部分に関して引数の有無に合わせてのcontentViewの変化するようにしたり、
ボタンの追加時に個数によってボーダーを追加するような実装にしました!

③ボタンに関して

a
public class DialogAction {
    public enum ActionStyle {
        case `default`
        case cancel
        case destructive
        case custom(color: UIColor, font: UIFont)
    }
    private var handler: ((_ action: DialogAction) -> Void)?
    public private(set) var style: ActionStyle
    public private(set) var title: String
    public var titleColor: UIColor {
        switch self.style {
            case .default: return UIColor.black
        case .cancel: return UIColor.black
            case .destructive: return UIColor.red
            case let .custom(color, _): return color
        }
    }
    public var titleFont: UIFont {
        switch self.style {
            case .default: return UIFont.boldSystemFont(ofSize: 14)
            case .cancel: return UIFont.systemFont(ofSize: 14)
        case .destructive: return UIFont.boldSystemFont(ofSize: 14)
            case let .custom(_, font): return font
        }
    }
    public init(title: String, style: ActionStyle, handler: ((_ action: DialogAction) -> Void)?) {
        self.handler = handler
        self.style = style
        self.title = title
    }
}

ボタンに関しては通常はdefault、cancel、destructiveの三つですが今回自由にカスタムできるように作って欲しいとのことだったのでcustomを追加してフォントとテキストカラーを自由に選べるようにしました!

④完成時の実装例

a
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()}

    @IBAction func `default`(_ sender: Any) {
        let alert = UIAlertController(title: "タイトル", message: "メッセージ", preferredStyle: .alert)
        let cancel = UIAlertAction(title: "キャンセル", style: .cancel,handler: nil)
        let ok = UIAlertAction(title: "OK", style: .default,handler: nil)
        alert.addAction(cancel)
        alert.addAction(ok)
        self.present(alert, animated: true, completion: nil)
    }


    @IBAction func Original(_ sender: Any) {
        let alert2 = DialogController(title: "タイトル", message:"メッセージ", banner:UIImage(imageLiteralResourceName:"Image" ) )
        let cancel2 = DialogAction(title: "キャンセル", style: .custom(color: UIColor.purple, font: UIFont.systemFont(ofSize: 16)), handler:nil )
        let ok2 = DialogAction(title: "Ok", style: .default,handler: nil)
        alert2.add(action: cancel2)
        alert2.add(action: ok2)
        self.present(alert2, animated: true, completion: nil)
    }

今回無事に実装できたのですが、animation関連がの知識不足を感じたので、また勉強していきたいなと思いました!🙆‍♂️

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1