Skip to content

Instantly share code, notes, and snippets.

@DonMag
Created March 31, 2022 18:57
Show Gist options
  • Save DonMag/3bad784b4c0d09dd1bbe6bcb19e10e37 to your computer and use it in GitHub Desktop.
Save DonMag/3bad784b4c0d09dd1bbe6bcb19e10e37 to your computer and use it in GitHub Desktop.
class NewPopupTestViewController: UIViewController {
let popupView = PopupView()
let testMessages: [String] = [
"Short Message",
"A Longer Message",
"A Sample Message that is long enough it will need to wrap onto multiple lines.",
]
var testIDX: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// a button to show the popup
let btn: UIButton = {
let b = UIButton()
b.backgroundColor = .systemRed
b.setTitle("Show Popup", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.addTarget(self, action: #selector(tapped(_:)), for: .touchUpInside)
return b
}()
// a couple labels at the top so we can see the popup blur effect
let label1: UILabel = {
let v = UILabel()
v.text = "Just some text to put near the top of the view"
v.backgroundColor = .yellow
v.textColor = .red
return v
}()
let label2: UILabel = {
let v = UILabel()
v.text = "so we can see that the popup covers it."
v.backgroundColor = .systemBlue
v.textColor = .white
return v
}()
[label1, label2].forEach { v in
v.font = .systemFont(ofSize: 24.0, weight: .light)
v.textAlignment = .center
v.numberOfLines = 0
}
[btn, label1, label2].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
label1.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
label1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
label2.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
label2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
label2.leadingAnchor.constraint(equalTo: label1.trailingAnchor, constant: 12.0),
label2.widthAnchor.constraint(equalTo: label1.widthAnchor),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btn.centerYAnchor.constraint(equalTo: g.centerYAnchor),
btn.widthAnchor.constraint(equalToConstant: 200.0),
btn.heightAnchor.constraint(equalToConstant: 50.0),
])
}
@objc func tapped(_ sender: Any) {
if popupView.superview != nil {
print("popup is already showing!")
return
}
// make sure we can load an image
if let img = UIImage(systemName: "checkmark.circle") {
let msg = testMessages[testIDX % testMessages.count]
popupView.showPopup(title: "Test \(testIDX)", message: msg, symbol: img)
testIDX += 1
}
}
}
class PopupView: UIView {
@IBOutlet weak var popupView: UIVisualEffectView!
@IBOutlet weak var symbol: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
// use a Timer instead of chainging animation blocks
private var timer: Timer?
// anim in/out duration
private var animDuration: TimeInterval = 0.3
// we'll swap these for the vertical position
private var topConstraint: NSLayoutConstraint!
private var botConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
private func configure() {
if let views = Bundle.main.loadNibNamed("PopupView", owner: self) {
guard let view = views.first as? UIView else { return }
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
NSLayoutConstraint.activate([
// constrain view loaded from xib to fill self
view.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
}
func findWindow() -> UIWindow? {
let scenes = UIApplication.shared.connectedScenes
guard let windowScene = scenes.first as? UIWindowScene,
let window = windowScene.windows.first
else {
return nil
}
return window
}
func showPopup(title: String, message: String, symbol: UIImage) {
self.titleLabel.text = title
self.descriptionLabel.text = message
self.symbol.image = symbol
// make sure background is clear so the VFX view works
self.backgroundColor = .clear
self.layer.cornerRadius = 20
self.clipsToBounds = true
configurePopup()
configureSwipeGesture()
// need to trigger animateIn *after* layout
DispatchQueue.main.async {
self.animateIn()
}
}
@objc private func animateOut() {
guard let sv = self.superview else { return }
self.topConstraint.isActive = false
self.botConstraint.isActive = true
UIView.animate(withDuration: animDuration, delay: 0.0, options: .curveEaseInOut) {
sv.layoutIfNeeded()
} completion: { [weak self] _ in
guard let self = self else { return }
self.removeFromSuperview()
}
}
private func animateIn() {
guard let sv = self.superview else { return }
self.botConstraint.isActive = false
self.topConstraint.isActive = true
UIView.animate(withDuration: animDuration, delay: 0.0, options: .curveEaseInOut) {
sv.layoutIfNeeded()
} completion: { [weak self] _ in
guard let self = self else { return }
self.timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.animateOut), userInfo: nil, repeats: false)
}
}
private func configurePopup() {
guard let window = findWindow() else { return }
self.translatesAutoresizingMaskIntoConstraints = false
window.addSubview(self)
// center horizontally
self.centerXAnchor.constraint(equalTo: window.centerXAnchor).isActive = true
// self.bottom above the top of the window (so, out of view)
botConstraint = self.bottomAnchor.constraint(equalTo: window.topAnchor, constant: -2.0)
// self.top 32-pts below the top of the window
topConstraint = self.topAnchor.constraint(equalTo: window.topAnchor, constant: 32.0)
// we start "out of view"
botConstraint.isActive = true
self.setNeedsLayout()
self.layoutIfNeeded()
}
private func configureSwipeGesture() {
// let's use a Pan Gesture instead of Swipe
// to make it more responsive
let pg = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.addGestureRecognizer(pg)
}
@objc private func handlePan(_ sender: UIPanGestureRecognizer) {
// to avoid multiple calls to animateOut,
// only execute if the timer is still running
if sender.translation(in: self).y < 0, let t = timer, t.isValid {
t.invalidate()
animateOut()
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PopupView" customModule="MoreScratch" customModuleProvider="target">
<connections>
<outlet property="descriptionLabel" destination="G3H-cQ-dH7" id="e6p-FX-jll"/>
<outlet property="popupView" destination="kse-pV-zuk" id="QH2-5G-wXQ"/>
<outlet property="symbol" destination="b6w-wL-w9q" id="MQy-yJ-Vhx"/>
<outlet property="titleLabel" destination="g0g-0z-EZb" id="9CK-Wd-aPm"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="228" height="70"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kse-pV-zuk">
<rect key="frame" x="0.0" y="0.0" width="220" height="50"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="6vZ-kb-RH4">
<rect key="frame" x="0.0" y="0.0" width="220" height="50"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="checkmark.circle.fill" translatesAutoresizingMaskIntoConstraints="NO" id="b6w-wL-w9q">
<rect key="frame" x="20" y="12.5" width="25" height="25"/>
<constraints>
<constraint firstAttribute="width" constant="25" id="hsU-PU-EkI"/>
<constraint firstAttribute="height" constant="25" id="wWO-aR-ic9"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" translatesAutoresizingMaskIntoConstraints="NO" id="YPm-FI-PjL">
<rect key="frame" x="50" y="10" width="165" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="749" text="Обновлено" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="7" translatesAutoresizingMaskIntoConstraints="NO" id="g0g-0z-EZb">
<rect key="frame" x="0.0" y="0.0" width="165" height="6"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Не удается загрузить данные. Проверьте соединение с интернетом" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="5" translatesAutoresizingMaskIntoConstraints="NO" id="G3H-cQ-dH7">
<rect key="frame" x="0.0" y="6" width="165" height="24"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" systemColor="systemGrayColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="YPm-FI-PjL" secondAttribute="trailing" constant="5" id="1Yo-EO-nK2"/>
<constraint firstItem="YPm-FI-PjL" firstAttribute="top" secondItem="6vZ-kb-RH4" secondAttribute="top" constant="10" id="SuN-Y6-5Wf"/>
<constraint firstItem="YPm-FI-PjL" firstAttribute="centerY" secondItem="6vZ-kb-RH4" secondAttribute="centerY" id="TV2-2R-2sN"/>
<constraint firstItem="YPm-FI-PjL" firstAttribute="leading" secondItem="b6w-wL-w9q" secondAttribute="trailing" constant="5" id="eYL-zj-ylR"/>
<constraint firstItem="b6w-wL-w9q" firstAttribute="centerY" secondItem="6vZ-kb-RH4" secondAttribute="centerY" id="f1F-1N-Vb7"/>
<constraint firstItem="b6w-wL-w9q" firstAttribute="leading" secondItem="6vZ-kb-RH4" secondAttribute="leading" constant="20" id="pPp-sB-ieS"/>
<constraint firstAttribute="bottom" secondItem="YPm-FI-PjL" secondAttribute="bottom" constant="10" id="zXZ-w4-cK6"/>
</constraints>
</view>
<constraints>
<constraint firstAttribute="height" constant="50" id="5bP-9J-KVD"/>
<constraint firstAttribute="width" constant="220" id="Jg0-fA-CDh"/>
</constraints>
<blurEffect style="systemUltraThinMaterial"/>
</visualEffectView>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="kse-pV-zuk" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="FW0-v4-nod"/>
<constraint firstItem="kse-pV-zuk" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="VVl-yN-Qmq"/>
<constraint firstAttribute="bottom" secondItem="kse-pV-zuk" secondAttribute="bottom" priority="999" id="Xhi-Jl-ciT"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="kse-pV-zuk" secondAttribute="trailing" priority="999" id="j9W-SN-tyG"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="122" y="-1083"/>
</view>
</objects>
<resources>
<image name="checkmark.circle.fill" width="11" height="11"/>
<systemColor name="systemGrayColor">
<color red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment