Created
March 31, 2022 18:57
-
-
Save DonMag/3bad784b4c0d09dd1bbe6bcb19e10e37 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
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 | |
} | |
} | |
} |
This file contains hidden or 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
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() | |
} | |
} | |
} |
This file contains hidden or 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="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