Last active
June 3, 2020 12:16
-
-
Save cristhianleonli/5c143190304af082c359bb8d80f34b53 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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