Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kopiev.ivan committed Oct 17, 2024
1 parent 4c426cb commit e88c575
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 58 deletions.

This file was deleted.

20 changes: 13 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "DeeplinkRouter",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "DeeplinkRouter",
targets: ["DeeplinkRouter"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "DeeplinkRouter"),
name: "DeeplinkRouter",
dependencies: [],
path: "Sources",
exclude: [],
resources: [],
swiftSettings: [],
linkerSettings: [
.linkedFramework("UIKit")
]
),
.testTarget(
name: "DeeplinkRouterTests",
dependencies: ["DeeplinkRouter"]),
dependencies: ["DeeplinkRouter"],
path: "Tests"
),
]
)
48 changes: 29 additions & 19 deletions Sources/DeeplinkRouter/BaseNavigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
//

import UIKit
import SafariServices

public final class BaseNavigator: NavigatorProtocol {

public var window: UIWindow? {
// Возвращает главное окно приложения, которое можно получить через делегат приложения
UIApplication.shared.windows.first { $0.isKeyWindow }
}
public var window: UIWindow?

public var topVc: UIViewController? {
// Возвращает верхний контроллер в стеке навигации
Expand All @@ -38,7 +36,13 @@ public final class BaseNavigator: NavigatorProtocol {
isLoading ? showLoader() : hideLoader()
}

public init() {}
public init(window: UIWindow? = nil) {
if let window = window {
self.window = window
} else {
self.window = UIApplication.shared.windows.first { $0.isKeyWindow }
}
}
}

// MARK: - Вспомогательные методы
Expand Down Expand Up @@ -74,29 +78,35 @@ private extension BaseNavigator {
loader.removeFromSuperview()
}

// Рекурсивный поиск верхнего контроллера
// Поиск верхнего контроллера
func topViewController(rootViewController: UIViewController?) -> UIViewController? {
guard let rootViewController = rootViewController else {
return nil
}
guard var vc = rootViewController else { return nil }

if let presented = rootViewController.presentedViewController {
return topViewController(rootViewController: presented)
// Если это UITabBarController, используем выбранный контроллер
if let tabBarController = vc as? UITabBarController, let selectedVC = tabBarController.selectedViewController {
vc = selectedVC
}

if let nav = rootViewController as? UINavigationController {
return topViewController(rootViewController: nav.visibleViewController)
// Если это UINavigationController, используем верхний контроллер
if let navController = vc as? UINavigationController, let topVC = navController.topViewController {
vc = topVC
}

if let tab = rootViewController as? UITabBarController {
return topViewController(rootViewController: tab.selectedViewController)
}
// Рекурсивно ищем presentedViewController
while let presentedVC = vc.presentedViewController {
if !(presentedVC is SFSafariViewController) && !(presentedVC is UIAlertController) {
vc = presentedVC
} else {
break
}

if let split = rootViewController as? UISplitViewController {
return topViewController(rootViewController: split.viewControllers.last)
// Если это UINavigationController внутри presentedViewController, проверяем его верхний контроллер
if let navController = vc as? UINavigationController, let topVC = navController.topViewController {
vc = topVC
}
}

return rootViewController
return vc
}

// Поиск контроллера по типу в иерархии
Expand Down
16 changes: 8 additions & 8 deletions Sources/DeeplinkRouter/DeeplinkRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,27 @@ public final class DeeplinkRouter {
}

extension DeeplinkRouter: DeeplinkRouterProtocol {
public func handle(deeplink: URL) {
guard isActive, UIApplication.shared.applicationState == .active else {
public func handle(deeplink: URL) async {
guard isActive else {
lastDeeplink = deeplink
return
}

for deeplinkType in deeplinkTypes {
if let handler = deeplinkType.canHandle(deeplink: deeplink) {
// Если тип может обработать URL, вызываем метод handle
Task {
await handler.handle(deeplink: deeplink, navigator: navigator)
}
await handler.handle(deeplink: deeplink, navigator: navigator)
lastDeeplink = nil
return
}
}
lastDeeplink = nil
print("No handler found for deeplink: \(deeplink)")
}

public func handleLastDeeplink() {
guard let lastDeeplink, isActive, UIApplication.shared.applicationState == .active else { return }
handle(deeplink: lastDeeplink)
public func handleLastDeeplink() async {
guard let lastDeeplink, isActive else { return }
await handle(deeplink: lastDeeplink)
}

public func register(deeplinks: [any AnyDeeplink.Type]) {
Expand Down
4 changes: 2 additions & 2 deletions Sources/DeeplinkRouter/Interface/DeeplinkRouterProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
public protocol DeeplinkRouterProtocol {
var isActive: Bool { get set }
var navigator: NavigatorProtocol { get set }
func handle(deeplink: URL)
func handleLastDeeplink()
func handle(deeplink: URL) async
func handleLastDeeplink() async
func register(deeplinks: [AnyDeeplink.Type])
}
2 changes: 1 addition & 1 deletion Sources/DeeplinkRouter/Interface/URLDecoderProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public protocol URLDecoderProtocol {
type: T.Type,
from url: URL,
decoder: JSONDecoder
) async throws -> T
) throws -> T
}
65 changes: 63 additions & 2 deletions Sources/DeeplinkRouter/URLDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,69 @@
//
// File.swift
//
// URLDecoder.swift
//
//
// Created by Иван Копиев on 17.10.2024.
//

import Foundation

// Класс для универсального парсинга URL в Codable объект
final class URLDecoder: URLDecoderProtocol {

// Универсальный метод для парсинга объекта Codable из URL query-параметров
func decode<T: URLCodable>(type: T.Type, from url: URL, decoder: JSONDecoder) throws -> T {
guard let queryParams = url.queryParameters else {
print("No query parameters found in URL")
throw URLError(.unsupportedURL)
}
// Преобразуем параметры в соответствующие типы на основе структуры Codable
let convertedParams = convertParameters(queryParams, for: type)
// Превращаем query-параметры в Data, подходящие для JSONDecoder
let jsonData = try JSONSerialization.data(withJSONObject: convertedParams, options: [])
let object = try decoder.decode(T.self, from: jsonData)
return object
}

// Преобразование query-параметров в соответствующие типы на основе структуры
private func convertParameters<T: URLCodable>(
_ parameters: [String: String],
for type: T.Type
) -> [String: Any] {
var convertedParams: [String: Any] = [:]

// Используем Mirror для рефлексии структуры
let mirror = Mirror(reflecting: type.template)

for (key, value) in parameters {
if let child = mirror.children.first(where: { $0.label == key }) {
// Преобразуем строку в нужный тип на основе метаданных
if child.value is Int, let intValue = Int(value) {
convertedParams[key] = intValue
} else if child.value is Double, let doubleValue = Double(value) {
convertedParams[key] = doubleValue
} else if child.value is Bool, let boolValue = Bool(value) {
convertedParams[key] = boolValue
} else {
// Если это строка или неизвестный тип, оставляем как строку
convertedParams[key] = value
}
}
}
return convertedParams
}
}

// Расширение URL для парсинга query-параметров
extension URL {
// Извлекаем словарь query-параметров
var queryParameters: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else { return nil }

var params: [String: String] = [:]
for item in queryItems {
params[item.name] = item.value
}
return params
}
}
89 changes: 86 additions & 3 deletions Tests/DeeplinkRouterTests/BaseNavigatorTests.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,91 @@
//
// File.swift
//
// BaseNavigatorTests.swift
//
//
// Created by Иван Копиев on 17.10.2024.
//

import Foundation
import XCTest
@testable import DeeplinkRouter

final class BaseNavigatorTests: XCTestCase {

var navigator: BaseNavigator!
var window: UIWindow!

override func setUp() {
super.setUp()
window = UIWindow()
navigator = BaseNavigator(window: window)
}

func testWindowReturnsKeyWindow() {
// Given
window.makeKeyAndVisible()

// When
let returnedWindow = navigator.window

// Then
XCTAssertEqual(returnedWindow, window)
}

func testTopVcReturnsCorrectViewController() {
// Given
let rootViewController = UIViewController()
window.rootViewController = rootViewController
window.makeKeyAndVisible()

// When
let topVc = navigator.topVc

// Then
XCTAssertEqual(topVc, rootViewController)
}

func testTopNavControllerReturnsCorrectNavigationController() {
// Given
let window = UIWindow()
navigator = BaseNavigator(window: window)
let navController = UINavigationController()
window.rootViewController = navController
window.makeKeyAndVisible()

// When
let topNavController = navigator.topNavController

// Then
XCTAssertEqual(topNavController, navController)
}

func testTabbarReturnsCorrectTabBarController() {
// Given
let tabBarController = UITabBarController()
window.rootViewController = tabBarController
window.makeKeyAndVisible()

// When
let tabbar = navigator.tabbar

// Then
XCTAssertEqual(tabbar, tabBarController)
}

@MainActor
func testSetLoadingShowsAndHidesLoader() {
// Given
window.makeKeyAndVisible()

// When
navigator.setLoading(true)

// Then
XCTAssertNotNil(window.viewWithTag(69))

// When
navigator.setLoading(false)

// Then
XCTAssertNil(window.viewWithTag(69))
}
}
Loading

0 comments on commit e88c575

Please sign in to comment.