Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- RxCocoa
- 애니메이션
- 스위프트
- map
- clean architecture
- Clean Code
- uiscrollview
- collectionview
- HIG
- ribs
- 리팩토링
- SWIFT
- tableView
- Protocol
- 클린 코드
- Human interface guide
- MVVM
- ios
- UICollectionView
- UITextView
- Xcode
- swift documentation
- rxswift
- 리펙토링
- Observable
- combine
- swiftUI
- uitableview
- Refactoring
- 리펙터링
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 커스텀 팝업 창 (custom popup, custom alert view) 본문
iOS 응용 (swift)
[iOS - swift] 커스텀 팝업 창 (custom popup, custom alert view)
jake-kim 2020. 11. 28. 14:55
커스텀 팝업 창 구현 아이디어
- UIViewController를 상속받아서 backgroundColror는 어둡게하고, 그 위에 customView를 띄우는 방식
- present로 통째로 띄우면 팝업이 표출되는 현상처럼 보이는 것을 활용
- 사용할때마다 객체로 만들며, UIViewController의 extension으로 넣고 ViewController에서 호출하여 사용
팝업을 띄우는 PopUpViewController 구현
- 필요한 stored property 프로퍼티
- 모두 optional로 설정하고 생성자에서 값을 받아서 초기화 > 만약 nil값이면 팝업 view에 addSubview를 하지 않게하여 표출되지 않게끔 설정
class PopUpViewController: UIViewController {
private var titleText: String?
private var messageText: String?
private var attributedMessageText: NSAttributedString?
private var contentView: UIView?
}
- 팝업 UI 구성: containerView, containerStackView, buttonStackView로 나누어서 구성
- containerView: containerStackView와 buttonStackView를 감싸서, 버튼, 타이틀 밖의 여백을 유지하기 위한 뷰
- containerStackView: 타이틀, buttonStackView를 감싸는 뷰
- buttonStackView: 버튼 2개를 가지고 있는 뷰
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 8
/// 팝업이 등장할 때(viewWillAppear)에서 containerView.transform = .identity로 하여 애니메이션 효과 주는 용도
view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
return view
}()
private lazy var containerStackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 12.0
view.alignment = .center
return view
}()
private lazy var buttonStackView: UIStackView = {
let view = UIStackView()
view.spacing = 14.0
view.distribution = .fillEqually
return view
}()
- title과 message label 정의
private lazy var titleLabel: UILabel? = {
let label = UILabel()
label.text = titleText
label.textAlignment = .center
label.font = .systemFont(ofSize: 18.0, weight: .bold)
label.numberOfLines = 0
label.textColor = .black
return label
}()
private lazy var messageLabel: UILabel? = {
guard messageText != nil || attributedMessageText != nil else { return nil }
let label = UILabel()
label.text = messageText
label.textAlignment = .center
label.font = .systemFont(ofSize: 16.0)
label.textColor = .gray
label.numberOfLines = 0
if let attributedMessageText = attributedMessageText {
label.attributedText = attributedMessageText
}
return label
}()
- 버튼은 따로 메소드에서 생성되게끔 설정 (UIViewController의 extension에서 title, titleColor 등을 주입하여 편리하게 생성하여 사용할수 있도록 구현
- button.addAction클로저와 image() 메소드는 맨 아래에 extension으로 기능 구현
public func addActionToButton(title: String? = nil,
titleColor: UIColor = .white,
backgroundColor: UIColor = .blue,
completion: (() -> Void)? = nil) {
let button = UIButton()
button.titleLabel?.font = .systemFont(ofSize: 16.0, weight: .bold)
// enable
button.setTitle(title, for: .normal)
button.setTitleColor(titleColor, for: .normal)
button.setBackgroundImage(backgroundColor.image(), for: .normal)
// disable
button.setTitleColor(.gray, for: .disabled)
button.setBackgroundImage(UIColor.gray.image(), for: .disabled)
// layer
button.layer.cornerRadius = 4.0
button.layer.masksToBounds = true
button.addAction(for: .touchUpInside) { _ in
completion?()
}
buttonStackView.addArrangedSubview(button)
}
- init 구현
- modalPresentaionStyle = .overFullScreen을 통해 해당 뷰가 present될때 화면을 덮도록 설정 (디폴트값은 pageSheet)
convenience init(titleText: String? = nil,
messageText: String? = nil,
attributedMessageText: NSAttributedString? = nil) {
self.init()
self.titleText = titleText
self.messageText = messageText
self.attributedMessageText = attributedMessageText
/// present 시 fullScreen (화면을 덮도록 설정) -> 설정 안하면 pageSheet 형태 (위가 좀 남아서 밑에 깔린 뷰가 보이는 형태)
modalPresentationStyle = .overFullScreen
}
convenience init(contentView: UIView) {
self.init()
self.contentView = contentView
modalPresentationStyle = .overFullScreen
}
- viewWillAppear, viewWillDisappear에서 팝업 표출/dismiss 애니메이션 구현
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// curveEaseOut: 시작은 천천히, 끝날 땐 빠르게
UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseOut) { [weak self] in
self?.containerView.transform = .identity
self?.containerView.isHidden = false
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// curveEaseIn: 시작은 빠르게, 끝날 땐 천천히
UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseIn) { [weak self] in
self?.containerView.transform = .identity
self?.containerView.isHidden = true
}
}
- UI 레이아웃, 기본 설정
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
addSubviews()
makeConstraints()
}
private func setupViews() {
view.addSubview(containerView)
containerView.addSubview(containerStackView)
view.backgroundColor = .black.withAlphaComponent(0.2)
}
private func addSubviews() {
view.addSubview(containerStackView)
if let contentView = contentView {
containerStackView.addSubview(contentView)
} else {
if let titleLabel = titleLabel {
containerStackView.addArrangedSubview(titleLabel)
}
if let messageLabel = messageLabel {
containerStackView.addArrangedSubview(messageLabel)
}
}
if let lastView = containerStackView.subviews.last {
containerStackView.setCustomSpacing(24.0, after: lastView)
}
containerStackView.addArrangedSubview(buttonStackView)
}
private func makeConstraints() {
containerView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.translatesAutoresizingMaskIntoConstraints = false
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 26),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -26),
containerView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 32),
containerView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -32),
containerStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 24),
containerStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 24),
containerStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -24),
containerStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -24),
buttonStackView.heightAnchor.constraint(equalToConstant: 48),
buttonStackView.widthAnchor.constraint(equalTo: containerStackView.widthAnchor)
])
}
- 위에서 필요한 메소드 extension으로 정의
// MARK: - Extension
extension UIColor {
/// Convert color to image
func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
return UIGraphicsImageRenderer(size: size).image { rendererContext in
self.setFill()
rendererContext.fill(CGRect(origin: .zero, size: size))
}
}
}
extension UIControl {
public typealias UIControlTargetClosure = (UIControl) -> ()
private class UIControlClosureWrapper: NSObject {
let closure: UIControlTargetClosure
init(_ closure: @escaping UIControlTargetClosure) {
self.closure = closure
}
}
private struct AssociatedKeys {
static var targetClosure = "targetClosure"
}
private var targetClosure: UIControlTargetClosure? {
get {
guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? UIControlClosureWrapper else { return nil }
return closureWrapper.closure
} set(newValue) {
guard let newValue = newValue else { return }
objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, UIControlClosureWrapper(newValue),
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
@objc func closureAction() {
guard let targetClosure = targetClosure else { return }
targetClosure(self)
}
public func addAction(for event: UIControl.Event, closure: @escaping UIControlTargetClosure) {
targetClosure = closure
addTarget(self, action: #selector(UIControl.closureAction), for: event)
}
}
UIViewController의 extension
extension UIViewController {
func showPopUp(title: String? = nil,
message: String? = nil,
attributedMessage: NSAttributedString? = nil,
leftActionTitle: String = "취소",
rightActionTitle: String = "확인",
leftActionCompletion: (() -> Void)? = nil,
rightActionCompletion: (() -> Void)? = nil) {
let popUpViewController = PopUpViewController(titleText: title,
messageText: message,
attributedMessageText: attributedMessage)
showPopUp(popUpViewController: popUpViewController,
leftActionTitle: leftActionTitle,
rightActionTitle: rightActionTitle,
leftActionCompletion: leftActionCompletion,
rightActionCompletion: rightActionCompletion)
}
func showPopUp(contentView: UIView,
leftActionTitle: String = "취소",
rightActionTitle: String = "확인",
leftActionCompletion: (() -> Void)? = nil,
rightActionCompletion: (() -> Void)? = nil) {
let popUpViewController = PopUpViewController(contentView: contentView)
showPopUp(popUpViewController: popUpViewController,
leftActionTitle: leftActionTitle,
rightActionTitle: rightActionTitle,
leftActionCompletion: leftActionCompletion,
rightActionCompletion: rightActionCompletion)
}
private func showPopUp(popUpViewController: PopUpViewController,
leftActionTitle: String,
rightActionTitle: String,
leftActionCompletion: (() -> Void)?,
rightActionCompletion: (() -> Void)?) {
popUpViewController.addActionToButton(title: leftActionTitle,
titleColor: .systemGray,
backgroundColor: .secondarySystemBackground) {
popUpViewController.dismiss(animated: false, completion: leftActionCompletion)
}
popUpViewController.addActionToButton(title: rightActionTitle,
titleColor: .white,
backgroundColor: .blue) {
popUpViewController.dismiss(animated: false, completion: rightActionCompletion)
}
present(popUpViewController, animated: false, completion: nil)
}
}
사용하는 쪽에서의 호출
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func didTapButton(_ sender: Any) {
showPopUp(title: "타이틀", message: "메세지")
}
}
* 전체 코드: https://github.com/JK0369/PopUpViewControllerExample
'iOS 응용 (swift)' 카테고리의 다른 글
Comments