Skip to content

Instantly share code, notes, and snippets.

@cristhianleonli
Last active June 3, 2020 12:16
Show Gist options
  • Save cristhianleonli/5c143190304af082c359bb8d80f34b53 to your computer and use it in GitHub Desktop.
Save cristhianleonli/5c143190304af082c359bb8d80f34b53 to your computer and use it in GitHub Desktop.
import UIKit
typealias ActionHandler = () -> Void
class Alert: UIView {
// MARK: - Outlets
@IBOutlet private var container: UIView!
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var subtitleLabel: UILabel!
@IBOutlet private var primaryButton: UIButton!
@IBOutlet private var secondaryButton: UIButton!
@IBOutlet private var widthConstraintLayout: NSLayoutConstraint!
@IBOutlet private var heightConstraintLayout: NSLayoutConstraint!
// MARK: - Actions
var onPrimaryTapped: ActionHandler = {}
var onSecondaryTapped: ActionHandler = {}
var onTapOutside: ActionHandler = {}
var onAutoClose: ActionHandler = {}
// MARK: - Properties
var model: AlertModel? {
didSet {
updateUI()
}
}
// MARK: - Life cycle
override func awakeFromNib() {
super.awakeFromNib()
setupUI()
setupObservers()
}
override func layoutSubviews() {
super.layoutSubviews()
self.primaryButton.layer.cornerRadius = self.primaryButton.frame.height / 2
self.secondaryButton.layer.cornerRadius = self.secondaryButton.frame.height / 2
}
}
// MARK: - Move to a respective file in the project
private extension Alert {
struct Colors {
static let title = UIColor(red: 70/255, green: 82/255, blue: 87/255, alpha: 1)
static let primary = UIColor(red: 59/255, green: 130/255, blue: 247/255, alpha: 1)
}
}
private extension Alert {
func updateUI() {
titleLabel.text = model?.title
subtitleLabel.text = model?.message
primaryButton.setTitle(model?.primaryButton, for: .normal)
secondaryButton.setTitle(model?.secondaryButton, for: .normal)
if model?.primaryButton == nil {
primaryButton.removeFromSuperview()
}
if model?.secondaryButton == nil {
secondaryButton.removeFromSuperview()
}
}
func setupUI() {
backgroundColor = UIColor.black.withAlphaComponent(0.3)
container.backgroundColor = .white
container.layer.shadowColor = UIColor.black.cgColor
container.layer.shadowOpacity = 0.1
container.layer.shadowOffset = .zero
container.layer.shadowRadius = 10
container.layer.cornerRadius = 10
titleLabel.textColor = Colors.title
subtitleLabel.textColor = Colors.title
secondaryButton.setTitleColor(Colors.title, for: .normal)
primaryButton.setTitleColor(.white, for: .normal)
primaryButton.backgroundColor = Colors.primary
secondaryButton.backgroundColor = .clear
}
func setupObservers() {
let gesture = UITapGestureRecognizer(target: self, action: #selector(tapped))
addGestureRecognizer(gesture)
}
@objc func tapped(_ sender: UITapGestureRecognizer) {
if model?.isDismissable ?? false {
self.onTapOutside()
self.popOut()
}
}
@IBAction func primaryButtonTapped(_ sender: Any) {
onPrimaryTapped()
popOut()
}
@IBAction func secondaryButtonTapped(_ sender: Any) {
onSecondaryTapped()
popOut()
}
func triggerAutoClose() {
guard let duration = model?.autoCloseDuration else {
return
}
switch duration {
case .quick:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.onAutoClose()
self?.popOut()
}
case .long:
DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
self?.onAutoClose()
self?.popOut()
}
case .none:
break
}
}
}
extension Alert {
static func instanceFromNib() -> Alert {
return UINib(nibName: "Alert", bundle: nil)
.instantiate(withOwner: nil, options: nil)[0] as! Alert
}
func show(in parent: UIView) {
parent.addSubview(self)
self.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: parent.topAnchor),
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
self.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
self.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
])
popIn()
}
func popIn() {
alpha = 0
self.widthConstraintLayout.constant = self.containerWidth
self.heightConstraintLayout.constant = self.containerHeight
container.transform = CGAffineTransform(scaleX: 0, y: 0)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
self.container.transform = .identity
self.alpha = 1
}) { _ in
self.triggerAutoClose()
}
}
func popOut() {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
self.container.transform = CGAffineTransform.identity.scaledBy(x: 0.1, y: 0.1)
self.alpha = 0
}) { _ in
self.removeFromSuperview()
}
}
var containerWidth: CGFloat {
return UIScreen.main.bounds.size.width * 0.9
}
var containerHeight: CGFloat {
return UIScreen.main.bounds.size.width * 0.6
}
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="Alert" customModule="Alerto" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="363" height="239"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8UR-Kn-M0X">
<rect key="frame" x="31.5" y="19.5" width="300" height="200"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="1000" verticalCompressionResistancePriority="1000" distribution="fillEqually" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="UHC-PZ-NiQ">
<rect key="frame" x="15" y="135" width="270" height="50"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M0Y-B7-3Ae">
<rect key="frame" x="0.0" y="0.0" width="131" height="50"/>
<fontDescription key="fontDescription" name="AvenirNext-Bold" family="Avenir Next" pointSize="17"/>
<state key="normal" title="Secondary"/>
<connections>
<action selector="secondaryButtonTapped:" destination="iN0-l3-epB" eventType="touchUpInside" id="8oz-zc-q12"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NdE-vI-3S7">
<rect key="frame" x="139" y="0.0" width="131" height="50"/>
<fontDescription key="fontDescription" name="AvenirNext-Bold" family="Avenir Next" pointSize="17"/>
<state key="normal" title="Primary"/>
<connections>
<action selector="primaryButtonTapped:" destination="iN0-l3-epB" eventType="touchUpInside" id="nIE-iZ-M99"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="50" id="mvu-i5-kw4"/>
</constraints>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iob-8i-JS9">
<rect key="frame" x="20" y="20" width="260" height="26"/>
<fontDescription key="fontDescription" name="Avenir-Heavy" family="Avenir" pointSize="19"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="250" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3gs-DQ-L8S">
<rect key="frame" x="10" y="61" width="280" height="20.5"/>
<fontDescription key="fontDescription" name="AvenirNext-Regular" family="Avenir Next" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemGray2Color" red="0.68235294120000001" green="0.68235294120000001" blue="0.69803921570000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="UHC-PZ-NiQ" secondAttribute="trailing" constant="15" id="0og-s4-qXN"/>
<constraint firstAttribute="bottom" secondItem="UHC-PZ-NiQ" secondAttribute="bottom" constant="15" id="0qw-Wz-rbl"/>
<constraint firstItem="iob-8i-JS9" firstAttribute="top" secondItem="8UR-Kn-M0X" secondAttribute="top" constant="20" id="5dh-Yq-bYF"/>
<constraint firstAttribute="trailing" secondItem="iob-8i-JS9" secondAttribute="trailing" constant="20" id="RZZ-C1-bie"/>
<constraint firstItem="3gs-DQ-L8S" firstAttribute="leading" secondItem="8UR-Kn-M0X" secondAttribute="leading" constant="10" id="Ub1-4Q-H5L"/>
<constraint firstAttribute="width" constant="300" id="dNS-fc-2No"/>
<constraint firstAttribute="height" constant="200" id="dkH-wi-wl1"/>
<constraint firstItem="iob-8i-JS9" firstAttribute="leading" secondItem="8UR-Kn-M0X" secondAttribute="leading" constant="20" id="em0-ok-0gt"/>
<constraint firstAttribute="trailing" secondItem="3gs-DQ-L8S" secondAttribute="trailing" constant="10" id="f0a-Db-jHv"/>
<constraint firstItem="3gs-DQ-L8S" firstAttribute="top" secondItem="iob-8i-JS9" secondAttribute="bottom" constant="15" id="uLN-RU-sju"/>
<constraint firstItem="UHC-PZ-NiQ" firstAttribute="leading" secondItem="8UR-Kn-M0X" secondAttribute="leading" constant="15" id="wGH-gg-GNR"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="8UR-Kn-M0X" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="fcS-fa-TQu"/>
<constraint firstItem="8UR-Kn-M0X" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="lbE-Xa-OYl"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="container" destination="8UR-Kn-M0X" id="ZfP-Pl-4PF"/>
<outlet property="heightConstraintLayout" destination="dkH-wi-wl1" id="9H3-9A-Rcx"/>
<outlet property="primaryButton" destination="NdE-vI-3S7" id="pUL-nm-nGh"/>
<outlet property="secondaryButton" destination="M0Y-B7-3Ae" id="aBO-02-78J"/>
<outlet property="subtitleLabel" destination="3gs-DQ-L8S" id="b5f-vA-5hD"/>
<outlet property="titleLabel" destination="iob-8i-JS9" id="I76-Ta-zk1"/>
<outlet property="widthConstraintLayout" destination="dNS-fc-2No" id="KCq-7F-BbJ"/>
</connections>
<point key="canvasLocation" x="99.275362318840592" y="-92.075892857142847"/>
</view>
</objects>
</document>
import Foundation
enum AutoCloseDuration: CaseIterable {
case none
case quick
case long
// can be extended for custom duration
}
struct AlertModel {
// MARK: - Properties
var title: String = ""
var message: String = ""
var primaryButton: String? = ""
var secondaryButton: String? = ""
var isDismissable: Bool = false
var autoCloseDuration: AutoCloseDuration
}
import UIKit
class ViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var titleTextField: UITextField!
@IBOutlet private var messageTextField: UITextField!
@IBOutlet private var primaryTextField: UITextField!
@IBOutlet private var secondaryTextField: UITextField!
@IBOutlet private var dismissSwitch: UISwitch!
@IBOutlet private var autoCloseSegment: UISegmentedControl!
@IBOutlet private var resultLabel: UILabel!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
[titleTextField, messageTextField, primaryTextField, secondaryTextField].forEach { field in
field?.clearButtonMode = .whileEditing
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fillDummyData()
}
}
private extension ViewController {
@IBAction func restoreValues(_ sender: Any) {
fillDummyData()
}
@IBAction func createAlert(_ sender: Any) {
let alert = Alert.instanceFromNib()
alert.model = createAlertModel()
alert.onSecondaryTapped = { [weak self] in
self?.resultLabel.text = "Secondary button pressed"
}
alert.onPrimaryTapped = { [weak self] in
self?.resultLabel.text = "Primary button pressed"
}
alert.onTapOutside = { [weak self] in
self?.resultLabel.text = "Tapped outside"
}
alert.onAutoClose = { [weak self] in
self?.resultLabel.text = "Auto closed"
}
alert.show(in: self.view)
}
func createAlertModel() -> AlertModel {
let model = AlertModel(
title: titleTextField.text ?? "",
message: messageTextField.text ?? "",
primaryButton: primaryTextField.text!.isEmpty ? nil : primaryTextField.text,
secondaryButton: secondaryTextField.text!.isEmpty ? nil : secondaryTextField.text,
isDismissable: dismissSwitch.isOn,
autoCloseDuration: AutoCloseDuration.allCases[autoCloseSegment.selectedSegmentIndex]
)
return model
}
func fillDummyData() {
titleTextField.text = "Do you really want to delete your account?"
messageTextField.text = "If you delete your account, you will lose you profile, messages, and phothos."
primaryTextField.text = "No, keep it"
secondaryTextField.text = "Yes, delete it"
dismissSwitch.isOn = true
autoCloseSegment.selectedSegmentIndex = 0
resultLabel.text = ""
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment