#はじめに
先日インターン先のタスクで共通で使う通知部分を実装する際、デフォルトのUIAlertControllerだとボタンのテキストをいじれなかったりとデザインに合わせることが難しかったので、一からUialertControllerに似た実装の仕方で作ってみたのでそれについてまとめてみようと思います!
######github
https://github.com/ikemoti/OriginalUIAlertController
######デザインの用件内容
・アラートのttitleやmessage部分のフォントやtextColorを変えたい
(デフォルトのアラートだと変更不可能)
・アラートの上部にバーナー画像を任意で追加可能にして欲しい
・ボタンの数を任意で選択できるようにしてフォントや文字色も選択可能にして欲しい
・なるべく使う際にUIAlerttControllerに似た使い方ができるようにして欲しい
#①アニメーション部分
はじめに土台となるアラートのアニメーション部分を作成しました!
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部分について
//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の変化するようにしたり、
ボタンの追加時に個数によってボーダーを追加するような実装にしました!
#③ボタンに関して
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を追加してフォントとテキストカラーを自由に選べるようにしました!
#④完成時の実装例
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関連がの知識不足を感じたので、また勉強していきたいなと思いました!🙆♂️