95. Парсим диплинки
Диплинк - это ссылка, ведущая пользователя на определенный экран в приложении. В этой статье расскажу как парсить, т.е. обрабатывать эти диплинки, чтобы открыть нужный экран.
План
- Настроим URL-схему для диплинков в нашем проекте по официальной документации
- Напишем код для обработки диплинков с параметрами и без
- Небольшой бонус 🎁
Настраиваем проект
- Открываем проект в
Xcode - Жмем на
project-файл слева в навигаторе - Жмем на таргет приложения, для которого будем настраивать диплинки
- Открываем вкладку Info
- Разворачиваем секцию URL Types
- Создаем схему для диплинков: заполняем идентификатор приложения и саму схему.
По идентификатору система будет находить приложение, которое нужно открыть для перехода по диплинку, но есть риск получить состояние неопределенности, когда у другого приложения идентификатор совпадает с вашим. Поэтому чем сложнее ваш идентификатор, тем лучше 😁.
Если для вас супер-важно иметь уникальный идентификатор, то есть смысл рассмотреть универсальные ссылки, тут официальная документация по ним.
Схема будет использоваться для создания всех диплинков для вашего приложения, т.е. для схемы "myapp" диплинк будет начинаться так: myapp://.
В нашем случае идентификатор будет com.oleg991.Shared-SwiftUl-Content, а схема - easydev991.
Пишем код
Код для этой статьи будет в отдельном пакете для удобства, но прежде чем написать код для обработки диплинка, нужно упомянуть, что в SwiftUI диплинк можно получить в модификаторе onOpenUrl, что существенно упрощает нам работу с диплинками по сравнению с AppDelegate, ведь теперь на любом экране можно подключить обработчик диплинков, супер-удобно.
Итак, мы добавляем модификатор onOpenUrl, в замыкании получаем URL с нашим диплинком, и теперь нужно понять, какой экран открывать.
Есть разные варианты реализации этой задачи. В большинстве случаев подойдет вариант с заранее известным набором путей, например enum Path, в котором будут все сценарии для диплинков в приложении.
Делаем с нуля
Создадим enum Path и сделаем в нем два сценария - для диплинка с параметром и без:
public enum Path: Equatable {
/// `easydev991://simple`
case simple
/// `easydev991://complex?id=<id>`
case complex(id: String)
public init?(host: String, params: [String: Any]) {
switch (host, params) {
case ("simple", _):
self = .simple
case let ("complex", params):
guard let id = params["id"] as? String else {
return nil
}
self = .complex(id: id)
default:
return nil
}
}
}
В инициализатор передаем часть URL, которая называется хостом - это основная часть диплинка, и дополнительно словарь с параметрами, которые диплинк нам предоставляет.
Вот такой у нас будет обработчик диплинков:
public struct DeeplinkParser {
private let url: URL
public init(url: URL) {
self.url = url
}
public var path: Path? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = components.host
else { return nil }
let params = components.queryItems?.reduce(into: [String: Any]()) { result, queryItem in
if let value = queryItem.value {
result[queryItem.name] = value
}
} ?? [:]
return Path(host: host, params: params)
}
}
Вся логика в одном месте - вычисляемое свойство Path, где мы достаем из URL хост и параметры, из которых пытаемся получить путь в приложении.
Теперь достаточно передать в обработчик ссылку в замыкании onOpenUrl и открыть нужный экран, пример с кодом будет в заключении - там стандартная работа со стейт-свойством и NavigationView/NavigationLink для навигации.
Делаем только обработку параметров
У вас в проекте уже может быть свой обработчик диплинков, и осталось только добавить обработку параметров. Для этого сценария можно использовать такой подход:
public struct DeeplinkParameters {
public let dictionary: [String: String]
public init(for url: URL) {
let urlString = url.absoluteString.removingPercentEncoding ?? url.absoluteString
let components = URLComponents(string: urlString)
dictionary = components?.queryItems?.reduce(into: [String: String]()) { result, key in
result[key.name.lowercased()] = key.value
} ?? [:]
}
public subscript(key: String) -> String? {
dictionary[key.lowercased()]
}
}
let url = URL(string: "myapp://complex?foo=bar&baz=qux")! let parameters = DeeplinkParameters(for: url) let foo = parameters["foo"] // Опциональная строка let bar = parameters["bar"] // Опциональная строка
Бонус
Пишем тесты для обеих структур с использованием Swift Testing.
Тесты для первой структуры
struct DeeplinkParserTests {
@Test func emptyURL() throws {
let url = try #require(URL(string: "myapp://"))
let parser = DeeplinkParser(url: url)
#expect(parser.path == nil)
}
@Test func simplePath() throws {
let url = try #require(URL(string: "myapp://simple"))
let parser = DeeplinkParser(url: url)
#expect(parser.path == .simple)
}
@Test func complexPathWithId() throws {
let url = try #require(URL(string: "myapp://complex?id=12345"))
let parser = DeeplinkParser(url: url)
#expect(parser.path == .complex(id: "12345"))
}
@Test func complexPathWithoutId() throws {
let url = try #require(URL(string: "myapp://complex"))
let parser = DeeplinkParser(url: url)
#expect(parser.path == nil)
}
@Test func invalidHost() throws {
let url = try #require(URL(string: "myapp://unknown"))
let parser = DeeplinkParser(url: url)
#expect(parser.path == nil)
}
@Test func malformedURL() throws {
let url = try #require(URL(string: "invalid-url"))
let parser = DeeplinkParser(url: url)
#expect(parser.path == nil)
}
}
Тесты для второй структуры
struct DeeplinkParametersTests {
@Test func emptyURL() throws {
let url = try #require(URL(string: "myapp://"))
let parameters = DeeplinkParameters(for: url)
#expect(parameters.dictionary.isEmpty)
}
@Test func singleQueryParameter() throws {
let url = try #require(URL(string: "myapp://path?param1=value1"))
let parameters = DeeplinkParameters(for: url)
#expect(parameters["param1"] == "value1")
}
@Test func multipleQueryParameters() throws {
let url = try #require(URL(string: "myapp://path?param1=value1¶m2=value2"))
let parameters = DeeplinkParameters(for: url)
#expect(parameters["param1"] == "value1")
#expect(parameters["param2"] == "value2")
}
@Test func caseInsensitiveParameterAccess() throws {
let url = try #require(URL(string: "myapp://path?Param1=VALUE1"))
let parameters = DeeplinkParameters(for: url)
#expect(parameters["param1"] == "VALUE1")
}
@Test func missingParameter() throws {
let url = try #require(URL(string: "myapp://path?param1=value1"))
let parameters = DeeplinkParameters(for: url)
#expect(parameters["param2"] == nil)
}
@Test func malformedURL() throws {
let url = try #require(URL(string: "invalid-url"))
let parameters = DeeplinkParameters(for: url)
#expect(parameters.dictionary.isEmpty)
}
}
Заключение
Тут можно почитать официальную документацию по настройке диплинков в iOS-приложении. Будет здорово, если после прочтения статьи/просмотра кода вам будет проще работать с диплинками в iOS 😎
Код для этой статьи можно посмотреть тут, а другие статьи - тут.