From af044aa591fbe7352d2df64d4b52ab5db791767a Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 4 Apr 2023 13:21:50 +0300 Subject: [PATCH] feat: integrate new base navigation deeplink handler --- .../BaseNavigationStackDeeplinkHandler.swift | 10 +- .../Deeplinks.xcplaygroundpage/Contents.swift | 96 +++++++++-------- TIDeeplink/Sources/TIDeeplinkService.swift | 10 +- docs/tideeplink/deeplinks.md | 102 +++++++++--------- 4 files changed, 113 insertions(+), 105 deletions(-) diff --git a/TIDeepLink/Sources/DeeplinkHandler/BaseNavigationStackDeeplinkHandler.swift b/TIDeepLink/Sources/DeeplinkHandler/BaseNavigationStackDeeplinkHandler.swift index 8893e4f5..bd1e2ace 100644 --- a/TIDeepLink/Sources/DeeplinkHandler/BaseNavigationStackDeeplinkHandler.swift +++ b/TIDeepLink/Sources/DeeplinkHandler/BaseNavigationStackDeeplinkHandler.swift @@ -26,21 +26,21 @@ import UIKit open class BaseNavigationStackDeeplinkHandler: DeeplinkHandler { - public var rootViewControllerKeeper: UIViewController + public var rootViewController: UIViewController public var asAnyHandlerClosure: Closure?> - public init(rootViewControllerKeeper: UIViewController, + public init(rootViewController: UIViewController, asAnyHandlerClosure: @escaping Closure?>) { - self.rootViewControllerKeeper = rootViewControllerKeeper + self.rootViewController = rootViewController self.asAnyHandlerClosure = asAnyHandlerClosure } // MARK: - DeeplinkHandler open func handle(deeplink: Deeplink) -> Operation? { - let handler = asAnyHandlerClosure(rootViewControllerKeeper) ?? findHandler(for: deeplink, - in: rootViewControllerKeeper) + let handler = asAnyHandlerClosure(rootViewController) ?? findHandler(for: deeplink, + in: rootViewController) return handler?.handle(deeplink: deeplink) } diff --git a/TIDeepLink/TIDeeplink.app/Contents/MacOS/TIDeeplink.playground/Pages/Deeplinks.xcplaygroundpage/Contents.swift b/TIDeepLink/TIDeeplink.app/Contents/MacOS/TIDeeplink.playground/Pages/Deeplinks.xcplaygroundpage/Contents.swift index 8a801417..80178578 100644 --- a/TIDeepLink/TIDeeplink.app/Contents/MacOS/TIDeeplink.playground/Pages/Deeplinks.xcplaygroundpage/Contents.swift +++ b/TIDeepLink/TIDeeplink.app/Contents/MacOS/TIDeeplink.playground/Pages/Deeplinks.xcplaygroundpage/Contents.swift @@ -5,34 +5,38 @@ ## Как настроить - 1. Создать представителей класса `Deeplink` + 1. Создать объект, представляющий диплинк. Такой объект должен соответствовать протоколу `Hashable` */ import Foundation import TIDeeplink import UIKit -extension Deeplink { - static var editProfile: Deeplink { - Deeplink(rawValue: "editProfile") - } - - static var office: Deeplink { - Deeplink(rawValue: "office") - } +enum ProjectDeeplink: Hashable { + case editProfile + case office(id: String) } //: 2. Создать объект, соответствующий протоколу `DeeplinkMapper`. Его задачей будет - преобразование _URL_ в диплинк final class ProjectDeeplinkMapper: DeeplinkMapper { - private let availableDeeplinks: [Deeplink] = [.office, .editProfile] + func map(url: URL) -> ProjectDeeplink? { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return nil + } - func map(url: URL) -> Deeplink? { - let stringData = parse(url) + switch components.host { + case "office": + if let id = components.path.split(separator: "/").last { + return .office(id: String(id)) + } - return availableDeeplinks.find(byRawValue: stringData) - } + return nil - private func parse(_ url: URL) -> String { - url.host ?? "" + case "editProfile": + return .editProfile + + default: + return nil + } } } @@ -42,45 +46,43 @@ class MainCoordinator: DeeplinkHandler { print("Presenting edit profile view controller") } - func openOfficesScreen() { - print("Presenting offices view controller") + func openOfficesScreen(forId id: String) { + print("Presenting offices view controller by id: \(id)") } // MARK: DeeplinkHandler - func handle(deeplink: Deeplink) -> Operation? { - if deeplink == .editProfile { + func handle(deeplink: ProjectDeeplink) -> Operation? { + switch deeplink { + case .editProfile: return BlockOperation { [weak self] in self?.openEditProfileScreen() } - } - if deeplink == .office { + case let .office(id): return BlockOperation { [weak self] in - self?.openOfficesScreen() + self?.openOfficesScreen(forId: id) } } - - return nil } } /*: **Опционально** 4. Создать сабкласс `TIDeeplinksService` для удобного использования по проекту */ - final class ProjectDeeplinksService: TIDeeplinksService { - static let shared = ProjectDeeplinksService() + final class ProjectDeeplinksService: TIDeeplinksService { + static let shared = ProjectDeeplinksService() } /*: - Создаем и передаем в сервис Mapper и Handler + Создаем и передаем в сервис Mapper и Handler. - > Так же можно воспользоваться инициализатором `init(mapper:handler:operationQueue:)` + > Так же можно воспользоваться инициализатором `init(mapper:handler:operationQueue:)`. Если Mapper и Handler передаются не в инициализаторе, то их самостоятельно необходимо привести к виду `AnyDeeplinkHandler` и `AnyDeeplinkMapper` с момощью методов: `eraseToAnyDeeplinkHandler()` и `eraseToAnyDeeplinkMapper()` */ let coordinator = MainCoordinator() let mapper = ProjectDeeplinkMapper() -ProjectDeeplinksService.shared.deeplinkMapper = mapper -ProjectDeeplinksService.shared.deeplinkHandler = coordinator +ProjectDeeplinksService.shared.deeplinkMapper = mapper.eraseToAnyDeeplinkMapper() +ProjectDeeplinksService.shared.deeplinkHandler = coordinator.eraseToAnyDeeplinkHandler() /*: В `AppDelegate` использвуем методы `deferredHandle(url:)` и `handlePendingDeeplinks()` для обработки диплинков. @@ -95,37 +97,45 @@ func application(_ app: UIApplication, open url: URL, options: [UIApplication.Op return true } -guard let url = URL(string: "app://office/") else { +guard let url = URL(string: "app://office/123") else { fatalError() } application(.shared, open: url) /*: - В качестве `DeeplinkHandler` можно использовать базовую реализацию `BaseNavigationStackDeeplinkHandler`, которая позволяет искать handler в иерархии view, способный обработать диплинк + В качестве `DeeplinkHandler` можно использовать базовую реализацию `BaseNavigationStackDeeplinkHandler`, которая позволяет искать handler в иерархии view, способный обработать диплинк. */ -class ViewController: UIViewController, DeeplinkHandler { - func handle(deeplink: Deeplink) -> Operation? { - if deeplink == .editProfile { + +protocol ProjectDeeplinkHandler: DeeplinkHandler where Deeplink == ProjectDeeplink { +} + +class ViewController: UIViewController, ProjectDeeplinkHandler { + func handle(deeplink: ProjectDeeplink) -> Operation? { + switch deeplink { + case .editProfile: return BlockOperation { print("Presenting edit profile view controller") } - } - if deeplink == .office { + case let .office(id): return BlockOperation { - print("Presenting offices view controller") + print("Presenting offices view controller by id: \(id)") } } - - return nil } } -//: Создание Handler. Для настройки передается rootViewController, который должен наследоваться от `UIViewController` и соответствовать протоколу `DeeplinkHandler`. С этого контроллера будет начинаться поиск обработчика +//: Создание Handler. Для настройки передается rootViewController, который должен наследоваться от `UIViewController` С этого контроллера будет начинаться поиск обработчика let viewController = ViewController() let navigationController = UINavigationController(rootViewController: viewController) -let handler = BaseNavigationStackDeeplinkHandler(rootViewControllerKeeper: navigationController) +let handler = BaseNavigationStackDeeplinkHandler(rootViewController: navigationController) { + guard let projectHandler = $0 as? (any ProjectDeeplinkHandler) else { + return nil + } + + return projectHandler.eraseToAnyDeeplinkHandler() +} //: Далее handler может передаваться для использования в `TIDeeplinksService` let service = TIDeeplinksService(mapper: mapper, handler: handler) diff --git a/TIDeeplink/Sources/TIDeeplinkService.swift b/TIDeeplink/Sources/TIDeeplinkService.swift index 7a5a4d8a..989d05e3 100644 --- a/TIDeeplink/Sources/TIDeeplinkService.swift +++ b/TIDeeplink/Sources/TIDeeplinkService.swift @@ -23,7 +23,9 @@ import Foundation import TIFoundationUtils -open class TIDeeplinksService { +open class TIDeeplinksService where Mapper.Deeplink == Deeplink, Handler.Deeplink == Deeplink { // MARK: - Private properties @@ -39,11 +41,7 @@ open class TIDeeplinksService { // MARK: - Init - public init( - mapper: Mapper? = nil, - handler: Handler? = nil, - operationQueue: OperationQueue = .main) where Mapper.Deeplink == Deeplink, Handler.Deeplink == Deeplink { - + public init(mapper: Mapper? = nil, handler: Handler? = nil, operationQueue: OperationQueue = .main) { self.deeplinkMapper = mapper?.eraseToAnyDeeplinkMapper() self.deeplinkHandler = handler?.eraseToAnyDeeplinkHandler() self.operationQueue = operationQueue diff --git a/docs/tideeplink/deeplinks.md b/docs/tideeplink/deeplinks.md index f474ee98..badcc464 100644 --- a/docs/tideeplink/deeplinks.md +++ b/docs/tideeplink/deeplinks.md @@ -5,21 +5,16 @@ ## Как настроить - 1. Создать представителей класса `Deeplink` + 1. Создать объект, представляющий диплинк. Такой объект должен соответствовать протоколу `Hashable` ```swift import Foundation import TIDeeplink import UIKit -extension Deeplink { - static var editProfile: Deeplink { - Deeplink(rawValue: "editProfile") - } - - static var office: Deeplink { - Deeplink(rawValue: "office") - } +enum ProjectDeeplink: Hashable { + case editProfile + case office(id: String) } ``` @@ -27,16 +22,25 @@ extension Deeplink { ```swift final class ProjectDeeplinkMapper: DeeplinkMapper { - private let availableDeeplinks: [Deeplink] = [.office, .editProfile] + func map(url: URL) -> ProjectDeeplink? { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return nil + } - func map(url: URL) -> Deeplink? { - let stringData = parse(url) + switch components.host { + case "office": + if let id = components.path.split(separator: "/").last { + return .office(id: String(id)) + } - return availableDeeplinks.find(byRawValue: stringData) - } + return nil - private func parse(_ url: URL) -> String { - url.host ?? "" + case "editProfile": + return .editProfile + + default: + return nil + } } } ``` @@ -49,26 +53,24 @@ class MainCoordinator: DeeplinkHandler { print("Presenting edit profile view controller") } - func openOfficesScreen() { - print("Presenting offices view controller") + func openOfficesScreen(forId id: String) { + print("Presenting offices view controller by id: \(id)") } // MARK: DeeplinkHandler - func handle(deeplink: Deeplink) -> Operation? { - if deeplink == .editProfile { + func handle(deeplink: ProjectDeeplink) -> Operation? { + switch deeplink { + case .editProfile: return BlockOperation { [weak self] in self?.openEditProfileScreen() } - } - if deeplink == .office { + case let .office(id): return BlockOperation { [weak self] in - self?.openOfficesScreen() + self?.openOfficesScreen(forId: id) } } - - return nil } } ``` @@ -76,20 +78,20 @@ class MainCoordinator: DeeplinkHandler { **Опционально** 4. Создать сабкласс `TIDeeplinksService` для удобного использования по проекту ```swift - final class ProjectDeeplinksService: TIDeeplinksService { - static let shared = ProjectDeeplinksService() + final class ProjectDeeplinksService: TIDeeplinksService { + static let shared = ProjectDeeplinksService() } ``` - Создаем и передаем в сервис Mapper и Handler + Создаем и передаем в сервис Mapper и Handler. - > Так же можно воспользоваться инициализатором `init(mapper:handler:operationQueue:)` + > Так же можно воспользоваться инициализатором `init(mapper:handler:operationQueue:)`. Если Mapper и Handler передаются не в инициализаторе, то их самостоятельно необходимо привести к виду `AnyDeeplinkHandler` и `AnyDeeplinkMapper` с момощью методов: `eraseToAnyDeeplinkHandler()` и `eraseToAnyDeeplinkMapper()` ```swift let coordinator = MainCoordinator() let mapper = ProjectDeeplinkMapper() -ProjectDeeplinksService.shared.deeplinkMapper = mapper -ProjectDeeplinksService.shared.deeplinkHandler = coordinator +ProjectDeeplinksService.shared.deeplinkMapper = mapper.eraseToAnyDeeplinkMapper() +ProjectDeeplinksService.shared.deeplinkHandler = coordinator.eraseToAnyDeeplinkHandler() ``` В `AppDelegate` использвуем методы `deferredHandle(url:)` и `handlePendingDeeplinks()` для обработки диплинков. @@ -105,50 +107,48 @@ func application(_ app: UIApplication, open url: URL, options: [UIApplication.Op return true } -guard let url = URL(string: "app://office/") else { +guard let url = URL(string: "app://office/123") else { fatalError() } application(.shared, open: url) ``` - В качестве `DeeplinkHandler` можно использовать базовую реализацию `BaseNavigationStackDeeplinkHandler`, которая позволяет искать handler в иерархии view, способный обработать диплинк + В качестве `DeeplinkHandler` можно использовать базовую реализацию `BaseNavigationStackDeeplinkHandler`, которая позволяет искать handler в иерархии view, способный обработать диплинк. ```swift -class NavigationController: UINavigationController, DeeplinkHandler { - - func handle(deeplink: Deeplink) -> Foundation.Operation? { - nil - } +protocol ProjectDeeplinkHandler: DeeplinkHandler where Deeplink == ProjectDeeplink { } -class ViewController: UIViewController, DeeplinkHandler { - func handle(deeplink: Deeplink) -> Operation? { - if deeplink == .editProfile { +class ViewController: UIViewController, ProjectDeeplinkHandler { + func handle(deeplink: ProjectDeeplink) -> Operation? { + switch deeplink { + case .editProfile: return BlockOperation { print("Presenting edit profile view controller") } - } - if deeplink == .office { + case let .office(id): return BlockOperation { - print("Presenting offices view controller") + print("Presenting offices view controller by id: \(id)") } } - - return nil } } ``` -Создание Handler. Для настройки передается rootViewController, который должен наследоваться от `UIViewController` и соответствовать протоколу `DeeplinkHandler`. С этого контроллера будет начинаться поиск обработчика +Создание Handler. Для настройки передается rootViewController, который должен наследоваться от `UIViewController` С этого контроллера будет начинаться поиск обработчика ```swift -typealias ProjectNavigationStackDeeplinkHandler = BaseNavigationStackDeeplinkHandler - let viewController = ViewController() -let navigationController = NavigationController(rootViewController: viewController) -let handler = ProjectNavigationStackDeeplinkHandler(rootViewControllerKeeper: navigationController) +let navigationController = UINavigationController(rootViewController: viewController) +let handler = BaseNavigationStackDeeplinkHandler(rootViewController: navigationController) { + guard let projectHandler = $0 as? (any ProjectDeeplinkHandler) else { + return nil + } + + return projectHandler.eraseToAnyDeeplinkHandler() +} ``` Далее handler может передаваться для использования в `TIDeeplinksService`