460 lines
17 KiB
Swift
460 lines
17 KiB
Swift
//
|
|
// SBUAlertView.swift
|
|
// SendbirdUIKit
|
|
//
|
|
// Created by Tez Park on 16/02/2020.
|
|
// Copyright © 2020 Sendbird, Inc. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
public typealias AlertButtonHandler = (_ info: Any?) -> Void
|
|
|
|
public protocol SBUAlertViewDelegate: AnyObject {
|
|
/// Called when `SBUAlertView` is dismiss
|
|
func didDismissAlertView()
|
|
}
|
|
|
|
public class SBUAlertButtonItem {
|
|
var title: String
|
|
var color: UIColor?
|
|
var completionHandler: AlertButtonHandler?
|
|
|
|
/// This function initializes alert button item.
|
|
/// - Parameters:
|
|
/// - title: Button's title text
|
|
/// - color: Button's title color
|
|
/// - completionHandler: Button's completion handler
|
|
public init(title: String,
|
|
color: UIColor? = nil,
|
|
completionHandler: @escaping AlertButtonHandler) {
|
|
self.title = title
|
|
self.color = color
|
|
self.completionHandler = completionHandler
|
|
}
|
|
}
|
|
|
|
public class SBUAlertView: NSObject {
|
|
static private let shared = SBUAlertView()
|
|
|
|
@SBUThemeWrapper(theme: SBUTheme.componentTheme)
|
|
var theme: SBUComponentTheme
|
|
|
|
var window: UIWindow?
|
|
var baseView = UIView()
|
|
var backgroundView = UIButton()
|
|
var inputField = UITextField()
|
|
var centerYRatio: CGFloat = 1.0
|
|
|
|
var confirmItem: SBUAlertButtonItem?
|
|
var cancelItem: SBUAlertButtonItem?
|
|
var dismissHandler: (() -> Void)?
|
|
|
|
let itemWidth: CGFloat = 270.0
|
|
let textInsideMargin: CGFloat = 3.0
|
|
let textTopBottomMargin: CGFloat = 20.0
|
|
let inputAreaHeight: CGFloat = 32.0
|
|
let inputAreaMargin: CGFloat = 32.0
|
|
let inputBottomMargin: CGFloat = 18.5
|
|
let buttonHeight: CGFloat = 44.0
|
|
let sideMargin: CGFloat = 16.0
|
|
|
|
let itemHeight: CGFloat = 40.0
|
|
let leftMargin: CGFloat = 14.0
|
|
let midMargin: CGFloat = 8.0
|
|
let rightMargin: CGFloat = 18.0
|
|
let topBottomMargin: CGFloat = 8.0
|
|
|
|
let bufferMargin: CGFloat = 8.0
|
|
|
|
var prevOrientation: UIDeviceOrientation = .unknown
|
|
|
|
weak var delegate: SBUAlertViewDelegate?
|
|
|
|
private override init() {
|
|
super.init()
|
|
}
|
|
|
|
/// This static function shows the alertView.
|
|
/// - Parameters:
|
|
/// - title: Title text
|
|
/// - message: Message text (default: nil)
|
|
/// - needInputField: If an input field is required, set value to `true`.
|
|
/// - placeHolder: Placeholder text (default: "")
|
|
/// - centerYRatio: AlertView's centerY ratio.
|
|
/// - oneTimetheme: One-time theme setting
|
|
/// - confirmButtonItem: Confirm button item
|
|
/// - cancelButtonItem: Cancel button item (nullable)
|
|
/// - delegate: AlertView delegate
|
|
public static func show(title: String,
|
|
message: String? = nil,
|
|
needInputField: Bool = false,
|
|
placeHolder: String? = "",
|
|
centerYRatio: CGFloat? = 1.0,
|
|
oneTimetheme: SBUComponentTheme? = nil,
|
|
confirmButtonItem: SBUAlertButtonItem,
|
|
cancelButtonItem: SBUAlertButtonItem?,
|
|
delegate: SBUAlertViewDelegate? = nil,
|
|
dismissHandler: (() -> Void)? = nil) {
|
|
self.shared.show(
|
|
title: title,
|
|
message: message,
|
|
needInputField: needInputField,
|
|
placeHolder: placeHolder,
|
|
centerYRatio: centerYRatio,
|
|
oneTimetheme: oneTimetheme,
|
|
confirmButtonItem: confirmButtonItem,
|
|
cancelButtonItem: cancelButtonItem,
|
|
delegate: delegate,
|
|
dismissHandler: dismissHandler
|
|
)
|
|
}
|
|
|
|
/// This static function dismissed the alert.
|
|
public static func dismiss() {
|
|
self.shared.dismiss()
|
|
}
|
|
|
|
private func show(title: String,
|
|
message: String? = nil,
|
|
needInputField: Bool = false,
|
|
placeHolder: String? = "",
|
|
centerYRatio: CGFloat? = 1.0,
|
|
oneTimetheme: SBUComponentTheme? = nil,
|
|
confirmButtonItem: SBUAlertButtonItem,
|
|
cancelButtonItem: SBUAlertButtonItem?,
|
|
delegate: SBUAlertViewDelegate?,
|
|
dismissHandler: (() -> Void)? = nil) {
|
|
|
|
self.delegate = delegate
|
|
|
|
self.handleDismiss(isUserInitiated: false)
|
|
|
|
self.prevOrientation = UIDevice.current.orientation
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(orientationChanged),
|
|
name: UIDevice.orientationDidChangeNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(keyboardWillShow),
|
|
name: UIResponder.keyboardWillShowNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(keyboardWillHide),
|
|
name: UIResponder.keyboardWillHideNotification,
|
|
object: nil
|
|
)
|
|
|
|
if let oneTimetheme = oneTimetheme {
|
|
self.theme = oneTimetheme
|
|
}
|
|
|
|
if let centerYRatio = centerYRatio {
|
|
self.centerYRatio = centerYRatio
|
|
}
|
|
|
|
self.window = UIApplication.shared.currentWindow
|
|
guard let window = self.window else { return }
|
|
|
|
self.confirmItem = confirmButtonItem
|
|
self.cancelItem = cancelButtonItem
|
|
self.dismissHandler = dismissHandler
|
|
|
|
// Set backgroundView
|
|
self.backgroundView.frame = self.window?.bounds ?? .zero
|
|
self.backgroundView.backgroundColor = theme.overlayColor
|
|
self.backgroundView.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
|
|
|
|
// Calc total height
|
|
var totalHeight: CGFloat = 0.0
|
|
let insideItemWidth = itemWidth - sideMargin*2
|
|
|
|
let titleHeight = self.getTextHeight(
|
|
text: title,
|
|
maxSize: CGSize(width: insideItemWidth, height: CGFloat.greatestFiniteMagnitude),
|
|
font: theme.alertTitleFont
|
|
)
|
|
totalHeight += titleHeight
|
|
|
|
var messageHeight: CGFloat = 0.0
|
|
if let message = message {
|
|
messageHeight = self.getTextHeight(
|
|
text: message,
|
|
maxSize: CGSize(width: insideItemWidth, height: CGFloat.greatestFiniteMagnitude),
|
|
font: theme.alertDetailFont
|
|
)
|
|
totalHeight += (textInsideMargin + messageHeight)
|
|
}
|
|
|
|
if needInputField {
|
|
// top, text-input, input, input-button
|
|
totalHeight += (inputAreaMargin + inputAreaMargin + inputAreaHeight + inputBottomMargin)
|
|
} else {
|
|
// top, text-button
|
|
totalHeight += (textTopBottomMargin + textTopBottomMargin)
|
|
}
|
|
|
|
totalHeight += buttonHeight
|
|
|
|
// Set baseView
|
|
self.baseView.frame = CGRect(
|
|
origin: .zero,
|
|
size: CGSize(width: self.itemWidth, height: totalHeight)
|
|
)
|
|
self.baseView.backgroundColor = theme.backgroundColor
|
|
|
|
// Set items
|
|
let titleLabel = UILabel(frame: CGRect(
|
|
origin: CGPoint(
|
|
x: sideMargin,
|
|
y: (needInputField ? inputAreaMargin : textTopBottomMargin)
|
|
),
|
|
size: CGSize(width: insideItemWidth, height: titleHeight))
|
|
)
|
|
titleLabel.numberOfLines = 0
|
|
titleLabel.font = theme.alertTitleFont
|
|
titleLabel.textColor = theme.alertTitleColor
|
|
titleLabel.text = title
|
|
titleLabel.textAlignment = .center
|
|
self.baseView.addSubview(titleLabel)
|
|
|
|
var originY = titleLabel.frame.maxY
|
|
|
|
if let message = message {
|
|
let messageLabel = UILabel(frame: CGRect(
|
|
origin: CGPoint(x: sideMargin, y: originY + textInsideMargin),
|
|
size: CGSize(width: insideItemWidth, height: messageHeight))
|
|
)
|
|
messageLabel.numberOfLines = 0
|
|
messageLabel.font = theme.alertDetailFont
|
|
messageLabel.textColor = theme.alertDetailColor
|
|
messageLabel.text = message
|
|
messageLabel.textAlignment = .center
|
|
self.baseView.addSubview(messageLabel)
|
|
originY = messageLabel.frame.maxY
|
|
}
|
|
|
|
if needInputField {
|
|
self.inputField = UITextField(frame: CGRect(
|
|
origin: CGPoint(x: sideMargin, y: originY + inputAreaMargin),
|
|
size: CGSize(width: insideItemWidth, height: inputAreaHeight))
|
|
)
|
|
inputField.backgroundColor = theme.alertTextFieldBackgroundColor
|
|
inputField.font = theme.alertTextFieldFont
|
|
inputField.textColor = theme.alertTitleColor
|
|
let paddingView: UIView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: 0))
|
|
inputField.placeholder = placeHolder
|
|
inputField.leftView = paddingView
|
|
inputField.leftViewMode = .always
|
|
inputField.rightView = paddingView
|
|
inputField.rightViewMode = .always
|
|
inputField.attributedPlaceholder = NSAttributedString(
|
|
string: placeHolder ?? "",
|
|
attributes: [NSAttributedString.Key.foregroundColor: theme.alertDetailColor]
|
|
)
|
|
self.baseView.addSubview(inputField)
|
|
originY = inputField.frame.maxY + inputBottomMargin
|
|
} else {
|
|
originY += textTopBottomMargin
|
|
}
|
|
|
|
let separator = UIView(frame: CGRect(
|
|
origin: CGPoint(x: 0, y: originY-0.5),
|
|
size: CGSize(width: itemWidth, height: 0.5))
|
|
)
|
|
separator.backgroundColor = theme.separatorColor
|
|
self.baseView.addSubview(separator)
|
|
|
|
let buttonWidth = (cancelButtonItem == nil) ? itemWidth : itemWidth/2
|
|
var buttonOriginX: CGFloat = 0.0
|
|
if let cancelButtonItem = cancelButtonItem {
|
|
let cancelButton = UIButton(
|
|
frame: CGRect(
|
|
origin: CGPoint(x: buttonOriginX, y: originY),
|
|
size: CGSize(width: buttonWidth, height: buttonHeight)
|
|
)
|
|
)
|
|
cancelButton.titleLabel?.font = theme.alertButtonFont
|
|
cancelButton.titleLabel?.textColor = theme.alertButtonColor
|
|
cancelButton.setTitle(cancelButtonItem.title, for: .normal)
|
|
cancelButton.setTitleColor(cancelButtonItem.color ?? theme.alertButtonColor, for: .normal)
|
|
cancelButton.addTarget(self, action: #selector(onClickAlertButton), for: .touchUpInside)
|
|
cancelButton.tag = 0
|
|
cancelButton.setBackgroundImage(UIImage.from(color: theme.backgroundColor), for: .normal)
|
|
cancelButton.setBackgroundImage(UIImage.from(color: theme.highlightedColor), for: .highlighted)
|
|
|
|
let separatorLine = UIView(frame: CGRect(
|
|
origin: cancelButton.frame.origin,
|
|
size: CGSize(width: 0.5, height: buttonHeight))
|
|
)
|
|
separatorLine.backgroundColor = theme.separatorColor
|
|
cancelButton.addSubview(separatorLine)
|
|
|
|
buttonOriginX += buttonWidth
|
|
|
|
self.baseView.addSubview(cancelButton)
|
|
}
|
|
|
|
let confirmButton = UIButton(frame: CGRect(
|
|
origin: CGPoint(x: buttonOriginX, y: originY),
|
|
size: CGSize(width: buttonWidth, height: buttonHeight))
|
|
)
|
|
confirmButton.titleLabel?.font = theme.alertButtonFont
|
|
confirmButton.setTitle(confirmButtonItem.title, for: .normal)
|
|
confirmButton.setTitleColor(confirmButtonItem.color ?? theme.alertButtonColor, for: .normal)
|
|
confirmButton.addTarget(self, action: #selector(onClickAlertButton), for: .touchUpInside)
|
|
confirmButton.tag = 1
|
|
confirmButton.setBackgroundImage(UIImage.from(color: theme.backgroundColor), for: .normal)
|
|
confirmButton.setBackgroundImage(UIImage.from(color: theme.highlightedColor), for: .highlighted)
|
|
self.baseView.addSubview(confirmButton)
|
|
|
|
// RoundRect
|
|
let rectShape = CAShapeLayer()
|
|
rectShape.bounds = self.baseView.frame
|
|
rectShape.position = self.baseView.center
|
|
rectShape.path = UIBezierPath(
|
|
roundedRect: self.baseView.bounds,
|
|
byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
|
|
cornerRadii: CGSize(width: 10, height: 10)
|
|
).cgPath
|
|
self.baseView.layer.mask = rectShape
|
|
|
|
if needInputField {
|
|
let rectShape = CAShapeLayer()
|
|
rectShape.bounds = self.inputField.frame
|
|
rectShape.position = self.inputField.center
|
|
rectShape.path = UIBezierPath(
|
|
roundedRect: self.inputField.bounds,
|
|
byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
|
|
cornerRadii: CGSize(width: 5, height: 5)
|
|
).cgPath
|
|
self.inputField.layer.mask = rectShape
|
|
}
|
|
|
|
// Add to window
|
|
window.addSubview(self.backgroundView)
|
|
self.baseView.center = CGPoint(
|
|
x: window.center.x,
|
|
y: window.center.y * self.centerYRatio
|
|
)
|
|
window.addSubview(self.baseView)
|
|
|
|
// Animation
|
|
let baseFrame = self.baseView.frame
|
|
self.baseView.frame = CGRect(
|
|
origin: CGPoint(x: baseFrame.origin.x, y: window.frame.height),
|
|
size: baseFrame.size
|
|
)
|
|
self.backgroundView.alpha = 0.0
|
|
UIView.animate(withDuration: 0.1, animations: {
|
|
self.backgroundView.alpha = 1.0
|
|
}) { _ in
|
|
self.baseView.frame = baseFrame
|
|
|
|
if needInputField {
|
|
self.inputField.becomeFirstResponder()
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func dismiss() {
|
|
handleDismiss(isUserInitiated: true)
|
|
}
|
|
|
|
@objc private func handleDismiss(isUserInitiated: Bool) {
|
|
for subView in self.baseView.subviews {
|
|
subView.removeFromSuperview()
|
|
}
|
|
|
|
self.confirmItem = nil
|
|
self.cancelItem = nil
|
|
|
|
self.inputField = UITextField()
|
|
self.backgroundView.removeFromSuperview()
|
|
self.baseView.removeFromSuperview()
|
|
|
|
NotificationCenter.default.removeObserver(
|
|
self,
|
|
name: UIDevice.orientationDidChangeNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.removeObserver(
|
|
self,
|
|
name: UIResponder.keyboardWillShowNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.removeObserver(
|
|
self,
|
|
name: UIResponder.keyboardWillHideNotification,
|
|
object: nil
|
|
)
|
|
|
|
if isUserInitiated {
|
|
self.delegate?.didDismissAlertView()
|
|
let handler = self.dismissHandler
|
|
self.dismissHandler = nil
|
|
handler?()
|
|
}
|
|
}
|
|
|
|
@objc private func keyboardWillShow(_ notification: Notification) {
|
|
guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
|
|
|
|
UIView.animate(withDuration: 0.1, animations: {
|
|
guard let window = self.window else { return }
|
|
|
|
self.baseView.center.y = ((window.frame.height - keyboardFrame.cgRectValue.height) / 2) * self.centerYRatio
|
|
})
|
|
}
|
|
|
|
@objc private func keyboardWillHide() {
|
|
UIView.animate(withDuration: 0.1, animations: {
|
|
guard let window = self.window else { return }
|
|
|
|
self.baseView.center.y = window.center.y * self.centerYRatio
|
|
})
|
|
}
|
|
|
|
// MARK: Common
|
|
private func getTextHeight(text: String, maxSize: CGSize, font: UIFont) -> CGFloat {
|
|
let rect = NSString(string: text).boundingRect(
|
|
with: maxSize,
|
|
options: NSStringDrawingOptions.usesLineFragmentOrigin,
|
|
attributes: [NSAttributedString.Key.font: font],
|
|
context: nil
|
|
)
|
|
return rect.height
|
|
}
|
|
|
|
// MARK: Button action
|
|
@objc private func onClickAlertButton(sender: UIButton) {
|
|
let index = sender.tag
|
|
if cancelItem != nil, index == 0 {
|
|
self.cancelItem?.completionHandler?(nil)
|
|
dismiss()
|
|
} else {
|
|
self.confirmItem?.completionHandler?(self.inputField.text)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
// MARK: Orientation
|
|
@objc
|
|
func orientationChanged(_ notification: NSNotification) {
|
|
let currentOrientation = UIDevice.current.orientation
|
|
|
|
if prevOrientation.isPortrait && currentOrientation.isLandscape ||
|
|
prevOrientation.isLandscape && currentOrientation.isPortrait {
|
|
dismiss()
|
|
}
|
|
|
|
self.prevOrientation = currentOrientation
|
|
}
|
|
}
|