Skip to content

Commit 1365914

Browse files
Fixed presenting the same instance of alert multiple times
1 parent fb09af0 commit 1365914

File tree

2 files changed

+110
-74
lines changed

2 files changed

+110
-74
lines changed

Example/StatusAlertDemoSwift/ViewController.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ class ViewController: UIViewController {
1616
}
1717

1818
fileprivate let cellReuseIdentifier = "reuseIdentifier"
19-
private let tableView = UITableView(frame: .zero,
20-
style: .grouped)
19+
private let tableView = UITableView(frame: .zero, style: .grouped)
2120

2221
fileprivate var sections: [Section] = []
2322
private var preferredPosition: StatusAlert.VerticalPosition = .center

Sources/StatusAlert/StatusAlert.swift

Lines changed: 109 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import UIKit
4848

4949
@objc public var message: String?
5050

51-
/// Determines wether `StatusAlert` can be picked or dismissed by tap
51+
/// Determines whether `StatusAlert` can be picked or dismissed by tap
5252
@objc public var canBePickedOrDismissed: Bool {
5353
get { return self.contentView.isUserInteractionEnabled }
5454
set { self.contentView.isUserInteractionEnabled = newValue }
@@ -60,6 +60,11 @@ import UIKit
6060
/// or to dismiss currently presented alerts if `multiplePresentationsBehavior` is `dismissCurrentlyPresented`
6161
private static var currentlyPresentedStatusAlerts: [StatusAlert] = []
6262

63+
private static var alertToPresent: StatusAlert? = nil
64+
private static var dismissing: Bool {
65+
return alertToPresent != nil
66+
}
67+
6368
private let defaultFadeAnimationDuration: TimeInterval = TimeInterval(UINavigationControllerHideShowBarDuration)
6469
private let blurEffect: UIBlurEffect = UIBlurEffect(style: .light)
6570

@@ -70,22 +75,16 @@ import UIKit
7075
private var titleLabel: UILabel? = nil
7176
private var messageLabel: UILabel? = nil
7277

78+
private var contentStackViewConstraints: [NSLayoutConstraint] = []
7379
private var reusableObjectsConstraints: [NSLayoutConstraint] = []
7480

7581
private var timer: Timer?
7682

77-
/// Determines whether `StatusAlert` can be shown
78-
private var canBeShown: Bool {
79-
if StatusAlert.multiplePresentationsBehavior == .ignoreIfAlreadyPresenting
80-
&& !StatusAlert.currentlyPresentedStatusAlerts.isEmpty {
81-
return false
82-
}
83-
if self.image == nil,
84-
self.title == nil,
85-
self.message == nil {
86-
return false
87-
}
88-
return true
83+
/// Determines whether `StatusAlert` has at least one item to show
84+
private var isContentEmpty: Bool {
85+
return self.image == nil
86+
&& self.title == nil
87+
&& self.message == nil
8988
}
9089

9190
/// Determines whether blur is available
@@ -122,16 +121,7 @@ import UIKit
122121
}
123122

124123
deinit {
125-
NSLayoutConstraint.deactivate(self.reusableObjectsConstraints)
126-
if let imageView = self.imageView {
127-
StatusAlert.reusableImageViewsManager.enqueueReusable(imageView)
128-
}
129-
if let titleLabel = self.titleLabel {
130-
StatusAlert.reusableLabelsManager.enqueueReusable(titleLabel)
131-
}
132-
if let messageLabel = self.messageLabel {
133-
StatusAlert.reusableLabelsManager.enqueueReusable(messageLabel)
134-
}
124+
self.enqueueReusableObjects()
135125
}
136126

137127
// MARK: - Static methods -
@@ -142,7 +132,7 @@ import UIKit
142132
/// - image: @1x should be 90*90 by default, optional
143133
/// - title: displayed beyond image
144134
/// - message: displayed beyond title or
145-
/// - canBePickedOrDismissed: determines wether StatusAlert can be picked or dismissed by tap
135+
/// - canBePickedOrDismissed: determines whether StatusAlert can be picked or dismissed by tap
146136
/// - Returns: `StatusAlert` instance
147137
@available(*, deprecated, message: "Use `init` instead and set all properties directly")
148138
@objc(statusAlertWithImage:title:message:canBePickedOrDismissed:)
@@ -280,16 +270,17 @@ import UIKit
280270
) {
281271

282272
self.assertIsMainThread()
283-
guard self.canBeShown else {
284-
return
273+
guard !self.isContentEmpty else { return }
274+
275+
self.prepareForPresentation { [weak self] in
276+
self?.prepareContent()
277+
self?.positionAlert(
278+
inPresenter: presenter,
279+
withVerticalPosition: verticalPosition,
280+
offset: offset
281+
)
282+
self?.performPresentation()
285283
}
286-
self.prepare()
287-
self.position(
288-
inPresenter: presenter,
289-
withVerticalPosition: verticalPosition,
290-
offset: offset
291-
)
292-
self.present()
293284
}
294285

295286
private func commonInit() {
@@ -387,12 +378,42 @@ import UIKit
387378
self.accessibilityElementsHidden = true
388379
self.accessibilityTraits = UIAccessibilityTraitNone
389380
}
381+
382+
private func resetView() {
383+
self.deactivateConstraints(&self.contentStackViewConstraints)
384+
385+
self.enqueueReusableObjects()
386+
}
387+
388+
private func enqueueReusableObjects() {
389+
self.deactivateConstraints(&self.reusableObjectsConstraints)
390+
if let imageView = self.imageView {
391+
imageView.removeFromSuperview()
392+
StatusAlert.reusableImageViewsManager.enqueueReusable(imageView)
393+
self.imageView = nil
394+
}
395+
if let titleLabel = self.titleLabel {
396+
titleLabel.removeFromSuperview()
397+
StatusAlert.reusableLabelsManager.enqueueReusable(titleLabel)
398+
self.titleLabel = nil
399+
}
400+
if let messageLabel = self.messageLabel {
401+
messageLabel.removeFromSuperview()
402+
StatusAlert.reusableLabelsManager.enqueueReusable(messageLabel)
403+
self.messageLabel = nil
404+
}
405+
}
406+
407+
private func deactivateConstraints(_ array: inout [NSLayoutConstraint]) {
408+
NSLayoutConstraint.deactivate(array)
409+
array = []
410+
}
390411

391412
// MARK: Creation methods
392413

393414
/// Must be called before the `StatusAlert` presenting
394-
private func prepare() {
395-
self.completeStackViewConstraints()
415+
private func prepareContent() {
416+
self.completeContentStackViewConstraints()
396417

397418
self.imageView = self.createImageViewIfNeeded()
398419
if let imageView = self.imageView {
@@ -434,7 +455,7 @@ import UIKit
434455
NSLayoutConstraint.activate(self.reusableObjectsConstraints)
435456
}
436457

437-
private func position(
458+
private func positionAlert(
438459
inPresenter presenter: UIView,
439460
withVerticalPosition verticalPosition: VerticalPosition,
440461
offset: CGFloat?
@@ -477,37 +498,42 @@ import UIKit
477498
}
478499
}
479500

480-
private func completeStackViewConstraints() {
501+
private func completeContentStackViewConstraints() {
502+
var constraints: [NSLayoutConstraint] = []
481503
if self.image != nil
482504
&& (self.title != nil || self.message != nil) {
483-
self.contentView.heightAnchor.constraint(
505+
506+
constraints.append(self.contentView.heightAnchor.constraint(
484507
greaterThanOrEqualToConstant: self.sizesAndDistances.minimumAlertHeight
485-
).isActive = true
486-
self.contentView.widthAnchor.constraint(
508+
))
509+
constraints.append(self.contentView.widthAnchor.constraint(
487510
equalToConstant: self.sizesAndDistances.defaultAlertWidth
488-
).isActive = true
489-
self.contentStackView.topAnchor.constraint(
511+
))
512+
constraints.append(self.contentStackView.topAnchor.constraint(
490513
greaterThanOrEqualTo: self.contentView.topAnchor,
491514
constant: self.sizesAndDistances.minimumStackViewTopSpace
492-
).isActive = true
493-
self.contentStackView.centerYAnchor.constraint(
515+
))
516+
constraints.append(self.contentStackView.centerYAnchor.constraint(
494517
equalTo: self.contentView.centerYAnchor,
495518
constant: (self.sizesAndDistances.minimumStackViewTopSpace - self.sizesAndDistances.minimumStackViewBottomSpace) / 2
496-
).isActive = true
519+
))
497520
} else {
498521
if self.image == nil {
499-
self.contentView.widthAnchor.constraint(
522+
constraints.append(self.contentView.widthAnchor.constraint(
500523
equalToConstant: self.sizesAndDistances.defaultAlertWidth
501-
).isActive = true
524+
))
502525
}
503-
self.contentStackView.topAnchor.constraint(
526+
constraints.append(self.contentStackView.topAnchor.constraint(
504527
greaterThanOrEqualTo: self.contentView.topAnchor,
505528
constant: self.sizesAndDistances.minimumStackViewBottomSpace
506-
).isActive = true
507-
self.contentStackView.centerYAnchor.constraint(
529+
))
530+
constraints.append(self.contentStackView.centerYAnchor.constraint(
508531
equalTo: self.contentView.centerYAnchor
509-
).isActive = true
532+
))
510533
}
534+
535+
self.contentStackViewConstraints.append(contentsOf: constraints)
536+
NSLayoutConstraint.activate(self.contentStackViewConstraints)
511537
}
512538

513539
@objc private func reduceTransparencyStatusDidChange() {
@@ -595,24 +621,35 @@ import UIKit
595621
}
596622

597623
// MARK: Presentation methods
598-
599-
private func present() {
624+
625+
private func prepareForPresentation(
626+
onPrepared: @escaping () -> Void
627+
) {
628+
600629
switch StatusAlert.multiplePresentationsBehavior {
601630
case .ignoreIfAlreadyPresenting:
602631
guard StatusAlert.currentlyPresentedStatusAlerts.isEmpty else { return }
603-
self.performPresentation()
632+
onPrepared()
604633
case .showMultiple:
605-
self.performPresentation()
634+
guard !StatusAlert.currentlyPresentedStatusAlerts.contains(self) else { return }
635+
onPrepared()
606636
case .dismissCurrentlyPresented:
607-
let group = DispatchGroup()
608-
for alert in StatusAlert.currentlyPresentedStatusAlerts {
609-
group.enter()
610-
alert.dismiss {
611-
group.leave()
637+
guard !StatusAlert.dismissing else { return }
638+
if !StatusAlert.currentlyPresentedStatusAlerts.isEmpty {
639+
StatusAlert.alertToPresent = self
640+
let group = DispatchGroup()
641+
for alert in StatusAlert.currentlyPresentedStatusAlerts {
642+
group.enter()
643+
alert.dismiss {
644+
group.leave()
645+
}
612646
}
613-
}
614-
group.notify(queue: DispatchQueue.main) { [weak self] in
615-
self?.performPresentation()
647+
group.notify(queue: DispatchQueue.main) {
648+
onPrepared()
649+
StatusAlert.alertToPresent = nil
650+
}
651+
} else {
652+
onPrepared()
616653
}
617654
}
618655
}
@@ -621,24 +658,23 @@ import UIKit
621658
StatusAlert.currentlyPresentedStatusAlerts.append(self)
622659

623660
let scale: CGFloat = self.sizesAndDistances.defaultInitialScale
624-
self.timer = Timer.scheduledTimer(
661+
let timer = Timer.scheduledTimer(
625662
timeInterval: self.alertShowingDuration - self.defaultFadeAnimationDuration,
626663
target: self,
627664
selector: #selector(self.dismissByTimer),
628665
userInfo: nil,
629666
repeats: false)
630-
if let timer = self.timer {
631-
RunLoop.main.add(
632-
timer,
633-
forMode: RunLoopMode.commonModes
634-
)
635-
}
667+
RunLoop.main.add(
668+
timer,
669+
forMode: RunLoopMode.commonModes
670+
)
671+
self.timer = timer
636672
self.contentView.transform = CGAffineTransform.identity.scaledBy(x: scale, y: scale)
637673

638674
UIView.animate(
639675
withDuration: self.defaultFadeAnimationDuration,
640676
delay: 0,
641-
options: UIViewAnimationOptions.curveEaseOut,
677+
options: .curveEaseOut,
642678
animations: {
643679
if self.isBlurAvailable {
644680
if #available(iOS 11, *) {
@@ -690,8 +726,9 @@ import UIKit
690726
if let strongSelf = self,
691727
let index = StatusAlert.currentlyPresentedStatusAlerts.index(of: strongSelf) {
692728
StatusAlert.currentlyPresentedStatusAlerts.remove(at: index)
693-
self?.removeFromSuperview()
694729
}
730+
self?.removeFromSuperview()
731+
self?.resetView()
695732
completion?()
696733
})
697734
}

0 commit comments

Comments
 (0)