Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions OBAKit/Orchestration/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public class Application: CoreApplication, PushServiceDelegate {

lazy var searchManager = SearchManager(application: self)

lazy var toastManager = ToastManager()

@objc lazy var userActivityBuilder = UserActivityBuilder(application: self)

/// Handles all deep-linking into the app.
Expand Down
13 changes: 9 additions & 4 deletions OBAKit/Surveys/Protocol/SurveyViewHostingProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import SafariServices
@MainActor
protocol SurveyViewHostingProtocol {

var application: Application { get }

var surveysVM: SurveysViewModel { get set }

var observationActive: Bool { get set }
Expand Down Expand Up @@ -84,9 +86,9 @@ extension SurveyViewHostingProtocol where Self: UIViewController {

switch toast.type {
case .error:
showErrorToast(toast.message)
self.showErrorToast(toast.message, using: self.application.toastManager)
case .success:
showSuccessToast(toast.message)
self.showSuccessToast(toast.message, using: self.application.toastManager)
}
} onChange: {
Task { @MainActor [weak self] in
Expand All @@ -102,7 +104,7 @@ extension SurveyViewHostingProtocol where Self: UIViewController {
self.surveysVM.openExternalSurvey,
let url = self.surveysVM.externalSurveyURL
else { return }

self.openSafari(with: url, router: router)
} onChange: {
Task { @MainActor [weak self] in
Expand Down Expand Up @@ -145,7 +147,10 @@ extension SurveyViewHostingProtocol where Self: UIViewController {
self?.observeSurveysState()
}

let hosting = UIHostingController(rootView: surveyQuestionsForm)
let hosting = UIHostingController(
rootView: surveyQuestionsForm
.environmentObject(application.toastManager)
)
router.present(hosting, from: self, isModal: true, isPopover: true)
}

Expand Down
2 changes: 0 additions & 2 deletions OBAKit/Surveys/ViewModel/SurveysAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ enum SurveysAction {

case onSkipSurvey

case hideToastMessage

// Questions Form

case onCloseQuestionsForm
Expand Down
3 changes: 0 additions & 3 deletions OBAKit/Surveys/ViewModel/SurveysViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ final public class SurveysViewModel {
case .dismissFullQuestionsForm:
showFullSurveyQuestions = false

case .hideToastMessage:
showToastMessage = false

case .hideSurveyDismissSheet:
showSurveyDismissSheet = false

Expand Down
3 changes: 1 addition & 2 deletions OBAKit/ToastMessageBar/ToastManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ import Foundation
import SwiftUI

class ToastManager: ObservableObject {
static let shared = ToastManager()

@Published var toast: Toast?
@Published var isShowing: Bool = false
private var workItem: DispatchWorkItem?

private init() {}
init() {}

func showSuccess(_ message: String, duration: TimeInterval = 3.0) {
let toast = Toast(message: message, type: .success, duration: duration)
Expand Down
64 changes: 35 additions & 29 deletions OBAKit/ToastMessageBar/View/ToastView+UIViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,64 @@ extension UIViewController {

private static var toastWindow: UIWindow?

func showSuccessToast(_ message: String?, duration: TimeInterval = 3.0) {
// MARK: - Public API

/// Shows a success toast. Pass `manager` to share the same ToastManager
/// instance that your SwiftUI views are observing (e.g. `application.toastManager`).
func showSuccessToast(_ message: String?, using manager: ToastManager, duration: TimeInterval = 3.0) {
guard let message, !message.isEmpty else { return }
showToast(message: message, type: .success, duration: duration)
showToast(message: message, type: .success, using: manager, duration: duration)
}

func showErrorToast(_ message: String?, duration: TimeInterval = 3.0) {
/// Shows an error toast. Pass `manager` to share the same ToastManager
/// instance that your SwiftUI views are observing (e.g. `application.toastManager`).
func showErrorToast(_ message: String?, using manager: ToastManager, duration: TimeInterval = 3.0) {
guard let message, !message.isEmpty else { return }
showToast(message: message, type: .error, duration: duration)
showToast(message: message, type: .error, using: manager, duration: duration)
}

private func showToast(message: String, type: Toast.ToastType, duration: TimeInterval) {
// MARK: - Private

private func showToast(message: String, type: Toast.ToastType, using manager: ToastManager, duration: TimeInterval) {
guard let windowScene = view.window?.windowScene else { return }

createToastWindowIfNeeded(windowScene)
createToastWindowIfNeeded(windowScene, manager: manager)

DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.5) {
UIViewController.toastWindow = nil
}

showToastFor(type, message: message, duration: duration)
}

private func createToastWindowIfNeeded(_ windowScene: UIWindowScene) {
if UIViewController.toastWindow == nil {
let window = UIWindow(windowScene: windowScene)
window.windowLevel = .alert
window.backgroundColor = .clear
window.isUserInteractionEnabled = false

let hostingController = UIHostingController(rootView: ToastContainerView())
hostingController.view.backgroundColor = .clear
window.rootViewController = hostingController
window.isHidden = false

UIViewController.toastWindow = window
}
}

private func showToastFor(_ type: Toast.ToastType, message: String, duration: TimeInterval) {
switch type {
case .success:
ToastManager.shared.showSuccess(message, duration: duration)
manager.showSuccess(message, duration: duration)
case .error:
ToastManager.shared.showError(message, duration: duration)
manager.showError(message, duration: duration)
}
}

private func createToastWindowIfNeeded(_ windowScene: UIWindowScene, manager: ToastManager) {
guard UIViewController.toastWindow == nil else { return }

let window = UIWindow(windowScene: windowScene)
window.windowLevel = .alert
window.backgroundColor = .clear
window.isUserInteractionEnabled = false

let hostingController = UIHostingController(
rootView: ToastContainerView().environmentObject(manager)
)

hostingController.view.backgroundColor = .clear
window.rootViewController = hostingController
window.isHidden = false

UIViewController.toastWindow = window
}
}

// MARK: - Toast Container View for UIKit
struct ToastContainerView: View {
@ObservedObject var manager = ToastManager.shared
@EnvironmentObject var manager: ToastManager

var body: some View {
if let toast = manager.toast {
Expand Down
2 changes: 1 addition & 1 deletion OBAKit/ToastMessageBar/View/ToastView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct ToastView: View {

struct ToastModifier: ViewModifier {

@ObservedObject var manager = ToastManager.shared
@EnvironmentObject var manager: ToastManager

let message: String?
let type: Toast.ToastType?
Expand Down
4 changes: 0 additions & 4 deletions OBAKitTests/Surveys/SurveysViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,4 @@ final class SurveysViewModelTests: XCTestCase {
expect(self.viewModel.showSurveyDismissSheet).to(beFalse())
}

func test_hideToastMessage_hidesToast() {
viewModel.onAction(.hideToastMessage)
expect(self.viewModel.showToastMessage).to(beFalse())
}
}
Loading