diff --git a/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift b/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift index 44ebd057eb..e403105e27 100644 --- a/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift +++ b/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift @@ -1,101 +1,112 @@ -@objc(MWMToast) +@objc final class Toast: NSObject { - @objc(MWMToastAlignment) + @objc enum Alignment: Int { case bottom case top } - private var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - private var timer: Timer? + private enum Constants { + static let presentationDuration: TimeInterval = 3 + static let animationDuration: TimeInterval = kDefaultAnimationDuration + static let bottomOffset: CGFloat = 63 + static let topOffset: CGFloat = 50 + static let horizontalOffset: CGFloat = 16 + static let labelOffsets = UIEdgeInsets(top: 10, left: 14, bottom: -10, right: -14) + static let maxWidth: CGFloat = 400 + } private static var toasts: [Toast] = [] + private var blurView: UIVisualEffectView - @objc static func toast(withText text: String) -> Toast { - let toast = Toast(text) - toasts.append(toast) - return toast - } - - @objc static func hideAll() { - toasts.forEach { $0.hide() } - } - private init(_ text: String) { - blurView.layer.setCorner(radius: 8) - blurView.clipsToBounds = true + blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + blurView.setStyle(.toastBackground) + blurView.isUserInteractionEnabled = false blurView.alpha = 0 + blurView.translatesAutoresizingMaskIntoConstraints = false let label = UILabel() label.text = text - label.textAlignment = .center + label.setStyle(.toastLabel) label.numberOfLines = 0 - label.font = .regular14() - label.textColor = .white label.translatesAutoresizingMaskIntoConstraints = false + label.setContentHuggingPriority(.defaultLow, for: .horizontal) blurView.contentView.addSubview(label) - blurView.isUserInteractionEnabled = false NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor, constant: 8), - label.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor, constant: -8), - label.topAnchor.constraint(equalTo: blurView.contentView.topAnchor, constant: 8), - label.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor, constant: -8) + label.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor, constant: Constants.labelOffsets.left), + label.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor, constant: Constants.labelOffsets.right), + label.topAnchor.constraint(equalTo: blurView.contentView.topAnchor, constant: Constants.labelOffsets.top), + label.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor, constant: Constants.labelOffsets.bottom) ]) } - - deinit { - timer?.invalidate() + + // MARK: - Public methods + + @objc + static func show(withText text: String) { + show(withText: text, alignment: .bottom) } - @objc func show() { - show(in: UIApplication.shared.keyWindow, alignment: .bottom) + @objc + static func show(withText text: String, alignment: Alignment) { + show(withText: text, alignment: alignment, pinToSafeArea: true) } - @objc func show(withAlignment alignment: Alignment, pinToSafeArea: Bool = true) { - show(in: UIApplication.shared.keyWindow, alignment: alignment, pinToSafeArea: pinToSafeArea) + @objc + static func show(withText text: String, alignment: Alignment, pinToSafeArea: Bool) { + let toast = Toast(text) + toasts.append(toast) + toast.show(withAlignment: alignment, pinToSafeArea: pinToSafeArea) } - @objc func show(in view: UIView?, alignment: Alignment, pinToSafeArea: Bool = true) { - guard let view = view else { return } - blurView.translatesAutoresizingMaskIntoConstraints = false + @objc + static func hideAll() { + toasts.forEach { $0.hide() } + } + + // MARK: - Private methods + + private func show(withAlignment alignment: Alignment, pinToSafeArea: Bool) { + Self.hideAll() + guard let view = UIApplication.shared.keyWindow else { return } view.addSubview(blurView) - let leadingConstraint = blurView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 16) - let trailingConstraint = blurView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16) - + let leadingConstraint = blurView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: Constants.horizontalOffset) + let trailingConstraint = blurView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -Constants.horizontalOffset) + let maxWidthConstraint = blurView.widthAnchor.constraint(equalToConstant: Constants.maxWidth).withPriority(.defaultLow) + + let verticalConstraint: NSLayoutConstraint + switch alignment { + case .bottom: + verticalConstraint = blurView.bottomAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.bottomAnchor : view.bottomAnchor, + constant: -Constants.bottomOffset) + case .top: + verticalConstraint = blurView.topAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.topAnchor : view.topAnchor, + constant: Constants.topOffset) + } + NSLayoutConstraint.activate([ leadingConstraint, - trailingConstraint - ]) - - let topConstraint: NSLayoutConstraint - if alignment == .bottom { - topConstraint = blurView.bottomAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.bottomAnchor : view.bottomAnchor, constant: -63) - } else { - topConstraint = blurView.topAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.topAnchor : view.topAnchor, constant: 50) - } - - NSLayoutConstraint.activate([ - topConstraint, + trailingConstraint, + maxWidthConstraint, + verticalConstraint, blurView.centerXAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.centerXAnchor : view.centerXAnchor) ]) - UIView.animate(withDuration: kDefaultAnimationDuration) { + UIView.animate(withDuration: Constants.animationDuration, animations: { self.blurView.alpha = 1 - } - - timer = Timer.scheduledTimer(timeInterval: 3, - target: self, - selector: #selector(hide), - userInfo: nil, - repeats: false) + } , completion: { _ in + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.presentationDuration) { + self.hide() + } + }) } - - @objc func hide() { - timer?.invalidate() + + private func hide() { if self.blurView.superview != nil { - UIView.animate(withDuration: kDefaultAnimationDuration, + UIView.animate(withDuration: Constants.animationDuration, animations: { self.blurView.alpha = 0 }) { [self] _ in self.blurView.removeFromSuperview() Self.toasts.removeAll(where: { $0 === self }) } diff --git a/iphone/Maps/Classes/CustomViews/MapViewControls/SideButtons/MWMSideButtons.mm b/iphone/Maps/Classes/CustomViews/MapViewControls/SideButtons/MWMSideButtons.mm index 1eab6ad453..f45a33d666 100644 --- a/iphone/Maps/Classes/CustomViews/MapViewControls/SideButtons/MWMSideButtons.mm +++ b/iphone/Maps/Classes/CustomViews/MapViewControls/SideButtons/MWMSideButtons.mm @@ -143,7 +143,7 @@ NSString * const kUDDidShowLongTapToShowSideButtonsToast = @"kUDDidShowLongTapTo - (void)setHidden:(BOOL)hidden { if (!self.hidden && hidden) - [[MWMToast toastWithText:L(@"long_tap_toast")] show]; + [Toast showWithText:L(@"long_tap_toast")]; return [self.sideView setHidden:hidden animated:YES]; } diff --git a/iphone/Maps/Classes/CustomViews/MapViewControls/TrafficButton/MWMTrafficButtonViewController.mm b/iphone/Maps/Classes/CustomViews/MapViewControls/TrafficButton/MWMTrafficButtonViewController.mm index 007a7c9949..65f28156b2 100644 --- a/iphone/Maps/Classes/CustomViews/MapViewControls/TrafficButton/MWMTrafficButtonViewController.mm +++ b/iphone/Maps/Classes/CustomViews/MapViewControls/TrafficButton/MWMTrafficButtonViewController.mm @@ -65,7 +65,7 @@ NSArray *imagesWithName(NSString *name) { - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - [MWMToast hideAll]; + [Toast hideAll]; } - (void)configLayout { @@ -115,7 +115,7 @@ NSArray *imagesWithName(NSString *name) { break; case MWMMapOverlayTrafficStateNoData: btn.imageName = @"btn_traffic_on"; - [[MWMToast toastWithText:L(@"traffic_data_unavailable")] show]; + [Toast showWithText:L(@"traffic_data_unavailable")]; break; case MWMMapOverlayTrafficStateNetworkError: [MWMMapOverlayManager setTrafficEnabled:NO]; @@ -123,11 +123,11 @@ NSArray *imagesWithName(NSString *name) { break; case MWMMapOverlayTrafficStateExpiredData: btn.imageName = @"btn_traffic_outdated"; - [[MWMToast toastWithText:L(@"traffic_update_maps_text")] show]; + [Toast showWithText:L(@"traffic_update_maps_text")]; break; case MWMMapOverlayTrafficStateExpiredApp: btn.imageName = @"btn_traffic_outdated"; - [[MWMToast toastWithText:L(@"traffic_update_app_message")] show]; + [Toast showWithText:L(@"traffic_update_app_message")]; break; } } @@ -138,7 +138,7 @@ NSArray *imagesWithName(NSString *name) { break; case MWMMapOverlayIsolinesStateEnabled: if (![MWMMapOverlayManager isolinesVisible]) - [[MWMToast toastWithText:L(@"isolines_toast_zooms_1_10")] show]; + [Toast showWithText:L(@"isolines_toast_zooms_1_10")]; break; case MWMMapOverlayIsolinesStateExpiredData: [MWMAlertViewController.activeAlertController presentInfoAlert:L(@"isolines_activation_error_dialog")]; @@ -162,7 +162,7 @@ NSArray *imagesWithName(NSString *name) { } else if ([MWMMapOverlayManager transitEnabled]) { btn.imageName = @"btn_subway_on"; if ([MWMMapOverlayManager transitState] == MWMMapOverlayTransitStateNoData) - [[MWMToast toastWithText:L(@"subway_data_unavailable")] show]; + [Toast showWithText:L(@"subway_data_unavailable")]; } else if ([MWMMapOverlayManager isoLinesEnabled]) { btn.imageName = @"btn_isoMap_on"; [self handleIsolinesState:[MWMMapOverlayManager isolinesState]]; diff --git a/iphone/Maps/Core/Theme/GlobalStyleSheet.swift b/iphone/Maps/Core/Theme/GlobalStyleSheet.swift index 60590d985b..3799fa0627 100644 --- a/iphone/Maps/Core/Theme/GlobalStyleSheet.swift +++ b/iphone/Maps/Core/Theme/GlobalStyleSheet.swift @@ -23,7 +23,6 @@ enum GlobalStyleSheet: String, CaseIterable { case trackRecordingWidgetButton = "TrackRecordingWidgetButton" case blackOpaqueBackground = "BlackOpaqueBackground" case blueBackground = "BlueBackground" - case toastBackground = "ToastBackground" case fadeBackground = "FadeBackground" case errorBackground = "ErrorBackground" case blackStatusBarBackground = "BlackStatusBarBackground" @@ -59,6 +58,8 @@ enum GlobalStyleSheet: String, CaseIterable { case white = "MWMWhite" case datePickerView = "DatePickerView" case valueStepperView = "ValueStepperView" + case toastBackground + case toastLabel } extension GlobalStyleSheet: IStyleSheet { @@ -194,10 +195,6 @@ extension GlobalStyleSheet: IStyleSheet { return .add { s in s.backgroundColor = colors.linkBlue } - case .toastBackground: - return .add { s in - s.backgroundColor = colors.toastBackground - } case .fadeBackground: return .add { s in s.backgroundColor = colors.fadeBackground @@ -429,6 +426,17 @@ extension GlobalStyleSheet: IStyleSheet { s.fontColor = colors.blackPrimaryText s.coloring = MWMButtonColoring.blue } + case .toastBackground: + return .add { s in + s.cornerRadius = 12 + s.clip = true + } + case .toastLabel: + return .add { s in + s.font = fonts.regular16 + s.fontColor = colors.whitePrimaryText + s.textAlignment = .center + } } } } diff --git a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift index 5012b2ed65..30ab1121ce 100644 --- a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift +++ b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift @@ -138,7 +138,7 @@ final class TrackRecordingManager: NSObject { private func stop(completion: (CompletionHandler)? = nil) { guard !trackRecorder.isTrackRecordingEmpty() else { - Toast.toast(withText: L("track_recording_toast_nothing_to_save")).show() + Toast.show(withText: L("track_recording_toast_nothing_to_save")) stopRecording(.withoutSaving, completion: completion) return } diff --git a/iphone/Maps/UI/Editor/MWMEditorViewController.mm b/iphone/Maps/UI/Editor/MWMEditorViewController.mm index 8964ee0976..c2a78e2d09 100644 --- a/iphone/Maps/UI/Editor/MWMEditorViewController.mm +++ b/iphone/Maps/UI/Editor/MWMEditorViewController.mm @@ -256,7 +256,7 @@ void registerCellsForTableView(std::vector const & cells, UITab - (void)showNotesQueuedToast { - [[MWMToast toastWithText:L(@"editor_edits_sent_message")] show]; + [Toast showWithText:L(@"editor_edits_sent_message")]; } #pragma mark - Headers diff --git a/iphone/Maps/UI/Help/AboutController/AboutController.swift b/iphone/Maps/UI/Help/AboutController/AboutController.swift index a0e4c07945..a010aa6abb 100644 --- a/iphone/Maps/UI/Help/AboutController/AboutController.swift +++ b/iphone/Maps/UI/Help/AboutController/AboutController.swift @@ -346,7 +346,7 @@ private extension AboutController { UIPasteboard.general.string = content let message = String(format: L("copied_to_clipboard"), content) UIImpactFeedbackGenerator(style: .medium).impactOccurred() - Toast.toast(withText: message).show(withAlignment: .bottom, pinToSafeArea: false) + Toast.show(withText: message, alignment: .bottom, pinToSafeArea: false) } } diff --git a/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift b/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift index ca2084c2ac..4fc72d3d94 100644 --- a/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift +++ b/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift @@ -59,7 +59,7 @@ final class DonationView: UIView { } } -private extension NSLayoutConstraint { +extension NSLayoutConstraint { func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint { self.priority = priority return self diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index bb2e8161b3..2919c85e17 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -129,7 +129,7 @@ extension PlacePageInteractor: PlacePageInfoViewControllerDelegate { UIPasteboard.general.string = content let message = String(format: L("copied_to_clipboard"), content) UIImpactFeedbackGenerator(style: .medium).impactOccurred() - Toast.toast(withText: message).show(withAlignment: .bottom) + Toast.show(withText: message, alignment: .bottom) } func didPressOpenInApp(from sourceView: UIView) { diff --git a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm index d4cf08a266..9a6e14e47f 100644 --- a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm +++ b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm @@ -254,12 +254,12 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS break; } case MWMBookmarksShareStatusEmptyCategory: - [[MWMToast toastWithText:L(@"bookmarks_error_title_share_empty")] show]; + [Toast showWithText:L(@"bookmarks_error_title_share_empty")]; isEnabled(NO); break; case MWMBookmarksShareStatusArchiveError: case MWMBookmarksShareStatusFileError: - [[MWMToast toastWithText:L(@"dialog_routing_system_error")] show]; + [Toast showWithText:L(@"dialog_routing_system_error")]; isEnabled(NO); break; }