diff --git a/.swiftpm/IIDadata.xctestplan b/.swiftpm/IIDadata.xctestplan
new file mode 100644
index 0000000..0a6019a
--- /dev/null
+++ b/.swiftpm/IIDadata.xctestplan
@@ -0,0 +1,34 @@
+{
+ "configurations" : [
+ {
+ "id" : "07C551C4-843E-4221-BA48-F07C2A0A0F21",
+ "name" : "Test Scheme Action",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+ "environmentVariableEntries" : [
+ {
+ "key" : "IIDadataAPIToken",
+ "value" : ""
+ }
+ ],
+ "targetForVariableExpansion" : {
+ "containerPath" : "container:",
+ "identifier" : "IIDadataTests",
+ "name" : "IIDadataTests"
+ }
+ },
+ "testTargets" : [
+ {
+ "target" : {
+ "containerPath" : "container:",
+ "identifier" : "IIDadataTests",
+ "name" : "IIDadataTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/.swiftpm/IIDadataTests.xctestplan b/.swiftpm/IIDadataTests.xctestplan
new file mode 100644
index 0000000..5978658
--- /dev/null
+++ b/.swiftpm/IIDadataTests.xctestplan
@@ -0,0 +1,24 @@
+{
+ "configurations" : [
+ {
+ "id" : "F9030ED7-7FF5-4CF2-B852-33FE599D3767",
+ "name" : "Test Scheme Action",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+
+ },
+ "testTargets" : [
+ {
+ "target" : {
+ "containerPath" : "container:",
+ "identifier" : "IIDadataTests",
+ "name" : "IIDadataTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Example.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Example.xcscheme
new file mode 100644
index 0000000..9619a1d
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Example.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata-Package.xcscheme
new file mode 100644
index 0000000..0861079
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata-Package.xcscheme
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/IIDadata.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata.xcscheme
similarity index 77%
rename from Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/IIDadata.xcscheme
rename to .swiftpm/xcode/xcshareddata/xcschemes/IIDadata.xcscheme
index 4dfd744..79a22c2 100644
--- a/Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/IIDadata.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata.xcscheme
@@ -1,10 +1,11 @@
+ LastUpgradeVersion = "1600"
+ version = "1.7">
+ buildImplicitDependencies = "YES"
+ buildArchitectures = "Automatic">
+ ReferencedContainer = "container:">
@@ -26,9 +27,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES">
-
-
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ shouldAutocreateTestPlan = "YES">
+ ReferencedContainer = "container:">
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/IIDadataUI.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/IIDadataUI.xcscheme
new file mode 100644
index 0000000..8e0638f
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/IIDadataUI.xcscheme
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/IIDadata-Test.swiftpm/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata SUI.xcscheme b/Example/IIDadata-Test.swiftpm/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata SUI.xcscheme
new file mode 100644
index 0000000..e6a7a91
--- /dev/null
+++ b/Example/IIDadata-Test.swiftpm/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata SUI.xcscheme
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/IIDadata-Test.swiftpm/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata-SPM.xcscheme b/Example/IIDadata-Test.swiftpm/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata-SPM.xcscheme
new file mode 100644
index 0000000..6e80183
--- /dev/null
+++ b/Example/IIDadata-Test.swiftpm/.swiftpm/xcode/xcshareddata/xcschemes/IIDadata-SPM.xcscheme
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/IIDadata-Test.swiftpm/ContentView.swift b/Example/IIDadata-Test.swiftpm/ContentView.swift
new file mode 100644
index 0000000..933ac3c
--- /dev/null
+++ b/Example/IIDadata-Test.swiftpm/ContentView.swift
@@ -0,0 +1,284 @@
+import SwiftUI
+import IIDadata
+import IIDadataUI
+
+struct ExampleView: View {
+ // Properties
+
+ let apiKey: String // = ProcessInfo.processInfo.environment["IIDadataAPIToken"] ?? ""
+
+ @StateObject private var dadata: DadataSuggestions
+ @State private var error: String? = nil
+ @State private var fio = ""
+ @State private var address = "Грибал"
+
+ @State private var fioSuggestions: [FioSuggestion]? = [
+ FioSuggestion(
+ "Иванов Иван Иванович",
+ unrestrictedValue: "Иванов Иван Иванович",
+ data: .init(surname: "Иванов", name: "Иван", patronymic: "Иванович", gender: .male, qc: nil)
+ )]
+ @State private var addressSuggestions: [AddressSuggestion]? = [
+ AddressSuggestion(
+ value: "Санкт-Петербург",
+ data: .init(city: "Санкт-Петербург"),
+ unrestrictedValue: "Санкт-Петербург"
+ ),
+ ]
+
+ @State private var isFioSuggestionsPresented = false
+ @State private var isAddressSuggestionsSuggestionsPresented = true
+
+ // Lifecycle
+
+ init() {
+ let apiKey = ProcessInfo.processInfo.environment["IIDadataAPIToken"] ?? ""
+ self.apiKey = apiKey
+ _dadata = StateObject(wrappedValue: DadataSuggestions(apiKey: apiKey))
+ }
+
+ // Content
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("**API key:** \(apiKey)").font(.subheadline)
+ if let error {
+ Text("**Error:** \(error)").font(.footnote)
+ .fontWeight(.medium).multilineTextAlignment(.center)
+ .foregroundStyle(.red)
+ }
+ Spacer()
+ TextField("Address", text: $address, prompt: Text("Enter address"))
+ .textFieldStyle(.roundedBorder)
+ .textContentType(.fullStreetAddress).tint(.blue)
+ .multilineTextAlignment(.leading)
+ .lineLimit(3)
+ .task(id: address, priority: .utility, getAddressSuggesttions)
+ .iidadataSuggestions(
+ apiKey: apiKey,
+ input: $address,
+ suggestions: $addressSuggestions,
+ isPresented: $isAddressSuggestionsSuggestionsPresented,
+ onSuggestionSelected: {
+ address = $0
+ if addressSuggestions?.count == 1 {
+ isFioSuggestionsPresented = false
+ isAddressSuggestionsSuggestionsPresented = false
+ }
+ })
+ TextField("FullName", text: $fio, prompt: Text("Enter FullName"))
+ .textFieldStyle(.roundedBorder)
+ .textContentType(.familyName).tint(.blue)
+ .multilineTextAlignment(.leading)
+ .lineLimit(3)
+ .task(id: fio, priority: .utility, getFioSuggesttions)
+ .iidadataSuggestions(
+ apiKey: apiKey,
+ input: $fio,
+ suggestions: $fioSuggestions,
+ isPresented: $isFioSuggestionsPresented,
+ onSuggestionSelected: {
+ fio = $0
+ if fioSuggestions?.count == 1 {
+ isFioSuggestionsPresented = false
+ isAddressSuggestionsSuggestionsPresented = false
+ }
+ })
+ Spacer()
+ }
+ .padding()
+ }
+
+ @ViewBuilder
+ func SuggestionsList(_ suggestions: [String], value: Binding) -> some View {
+ if !suggestions.isEmpty {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(suggestions, id: \.self) { suggestion in
+ Button(suggestion) {
+ value.wrappedValue = suggestion
+ if suggestions.count == 1 {
+ isFioSuggestionsPresented = false
+ isAddressSuggestionsSuggestionsPresented = false
+ }
+ }
+ .font(.body)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .frame(maxWidth: UIScreen.main.bounds.width - 44, alignment: .leading)
+ .tint(.secondary)
+ .multilineTextAlignment(.leading)
+ .safeAreaInset(edge: .top) {
+ Divider()
+ }
+ }
+ }.padding().fixedSize(horizontal: false, vertical: true)
+ }
+ }
+ }
+
+ // Functions
+
+ @Sendable @MainActor func getFioSuggesttions() async {
+ guard !fio.isEmpty else { return }
+ do {
+ error = nil
+ let suggestedFioResponce = try await dadata.suggestFio(
+ fio,
+ count: 10,
+ gender: .male, parts: [FioSuggestionQuery.Part.surname, .name, .patronymic]
+ )
+ guard
+ !suggestedFioResponce.isEmpty
+ else {
+ throw DecodingError.dataCorrupted(
+ .init(codingPath: [],
+ debugDescription: "Suggested FIO Responce is Empty")
+ )
+ }
+ fioSuggestions = suggestedFioResponce
+ isFioSuggestionsPresented = true
+ } catch {
+ isFioSuggestionsPresented = false
+ self.error = String(reflecting: error)
+ }
+ }
+
+ @Sendable @MainActor func getAddressSuggesttions() async {
+ guard !address.isEmpty else { return }
+ do {
+ error = nil
+ let suggestedAddressResponce = try await dadata.suggestAddress(
+ address,
+ queryType: .address,
+ resultsCount: 10,
+ language: .ru,
+ upperScaleLimit: .city,
+ lowerScaleLimit: .flat,
+ trimRegionResult: true
+ )
+ guard let addressSuggestions = suggestedAddressResponce.suggestions,
+ !addressSuggestions.isEmpty
+ else {
+ throw DecodingError.dataCorrupted(
+ .init(
+ codingPath: [],
+ debugDescription: "Suggested Address Responce is Empty or Nil"
+ )
+ )
+ }
+ self.addressSuggestions = addressSuggestions
+ isAddressSuggestionsSuggestionsPresented = true
+ } catch {
+ isAddressSuggestionsSuggestionsPresented = false
+ self.error = String(reflecting: error)
+ }
+ }
+}
+
+#Preview {
+ ExampleView()
+}
+
+//// MARK: - ContentView
+//
+///// A sample view demonstrating the usage of `IIDadataSuggestionsView`.
+//struct ContentView: View {
+// // Nested Types
+//
+// // MARK: - IIDadataViewModel
+//
+// /// The view model for managing address and FIO suggestions.
+// class IIDadataViewModel: ObservableObject {
+// // Properties
+//
+// @Published var address = ""
+// @Published var fio = ""
+// @Published var addressSuggestions: [AddressSuggestion]?
+// @Published var fioSuggestions: [FioSuggestion]?
+//
+// private let dadata: DadataSuggestions
+//
+// // Lifecycle
+//
+// /// Initializes the `IIDadataViewModel` with the appropriate API key.
+// ///
+// /// The API key is fetched from the environment variables.
+// init() {
+// let apiKey = ProcessInfo.processInfo.environment["IIDadataAPIToken"] ?? ""
+// dadata = DadataSuggestions(apiKey: apiKey)
+// }
+//
+// // Functions
+//
+// /// Fetches address suggestions based on the current address input.
+// ///
+// /// This function is called asynchronously and updates the `addressSuggestions` property.
+// /// It performs a check to ensure the address input is not empty before fetching suggestions.
+// @MainActor
+// func getAddressSuggestions() async {
+// guard !address.isEmpty else { return }
+// do {
+// addressSuggestions = try await dadata.suggestAddress(
+// address,
+// queryType: .address,
+// resultsCount: 10,
+// language: .ru
+// ).suggestions
+// } catch {
+// print("Error fetching address suggestions: \(error)")
+// }
+// }
+//
+// /// Fetches FIO (Full Name) suggestions based on the current FIO input.
+// ///
+// /// This function is called asynchronously and updates the `fioSuggestions` property.
+// /// It performs a check to ensure the FIO input is not empty before fetching suggestions.
+// @MainActor
+// func getFioSuggestions() async {
+// guard !fio.isEmpty else { return }
+// do {
+// fioSuggestions = try await dadata.suggestFio(
+// fio,
+// count: 10,
+// gender: .male,
+// parts: [.surname, .name, .patronymic]
+// )
+// } catch {
+// print("Error fetching FIO suggestions: \(error)")
+// }
+// }
+// }
+//
+// // Properties
+//
+// @StateObject private var viewModel = IIDadataViewModel()
+//
+// // Content
+//
+// var body: some View {
+// VStack(spacing: 16) {
+// IIDadataSuggestionsView(
+// inputText: $viewModel.address,
+// suggestions: $viewModel.addressSuggestions,
+// placeholder: "Enter address",
+// onSuggestionSelected: { _ in },
+// getSuggestions: viewModel.getAddressSuggestions
+// )
+// IIDadataSuggestionsView(
+// inputText: $viewModel.fio,
+// suggestions: $viewModel.fioSuggestions,
+// placeholder: "Enter Full Name",
+// onSuggestionSelected: { _ in },
+// getSuggestions: viewModel.getFioSuggestions
+// )
+// }
+// .padding()
+// }
+//}
+//
+//struct ContentView_Previews: PreviewProvider {
+// static var previews: some View {
+// ContentView()
+// }
+//}
diff --git a/Example/IIDadata-Test.swiftpm/MyApp.swift b/Example/IIDadata-Test.swiftpm/MyApp.swift
new file mode 100644
index 0000000..480e02a
--- /dev/null
+++ b/Example/IIDadata-Test.swiftpm/MyApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct MyApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ExampleView()
+ }
+ }
+}
diff --git a/Example/IIDadata-Test.swiftpm/Package.swift b/Example/IIDadata-Test.swiftpm/Package.swift
new file mode 100644
index 0000000..ae59f70
--- /dev/null
+++ b/Example/IIDadata-Test.swiftpm/Package.swift
@@ -0,0 +1,62 @@
+// swift-tools-version: 6.0
+
+// WARNING:
+// This file is automatically generated.
+// Do not edit it by hand because the contents will be replaced.
+
+import PackageDescription
+import AppleProductTypes
+
+let package = Package(
+ name: "IIDadata SUI",
+ platforms: [
+ .iOS("16.0")
+ ],
+ products: [
+ .iOSApplication(
+ name: "IIDadata SUI",
+ targets: ["AppModule"],
+ bundleIdentifier: "spm.swiftui.IIDadata-Tests.IntegrationTest",
+ teamIdentifier: "UB936SP78M",
+ displayVersion: "1.0",
+ bundleVersion: "1",
+ appIcon: .placeholder(icon: .sparkle),
+ accentColor: .presetColor(.yellow),
+ supportedDeviceFamilies: [
+ .pad,
+ .phone
+ ],
+ supportedInterfaceOrientations: [
+ .portrait,
+ .landscapeRight,
+ .landscapeLeft,
+ .portraitUpsideDown(.when(deviceFamilies: [.pad]))
+ ],
+ capabilities: [
+ .appTransportSecurity(configuration: .init(
+ exceptionDomains: [
+ .init(
+ domainName: "suggestions.dadata.ru",
+ includesSubdomains: true,
+ exceptionAllowsInsecureHTTPLoads: true
+ )
+ ]
+ )),
+ .outgoingNetworkConnections()
+ ],
+ appCategory: .developerTools
+ )
+ ],
+ dependencies: [
+ .package(path: "../..")
+ ],
+ targets: [
+ .executableTarget(
+ name: "AppModule",
+ dependencies: [
+ .product(name: "IIDadata", package: "iidadata")
+ ],
+ path: "."
+ )
+ ]
+)
\ No newline at end of file
diff --git a/Example/IIDadata.xcodeproj/project.pbxproj b/Example/IIDadata.xcodeproj/project.pbxproj
index 165ae6b..f5a06dd 100644
--- a/Example/IIDadata.xcodeproj/project.pbxproj
+++ b/Example/IIDadata.xcodeproj/project.pbxproj
@@ -52,6 +52,7 @@
7F635716033EB85D6E45087A /* Pods-IIDadata_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IIDadata_Tests.release.xcconfig"; path = "Target Support Files/Pods-IIDadata_Tests/Pods-IIDadata_Tests.release.xcconfig"; sourceTree = ""; };
BB65EEE2EDECA8BF67A58EF2 /* IIDadata.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = IIDadata.podspec; path = ../IIDadata.podspec; sourceTree = ""; };
E749FED1565D30A1C1AC21CB /* Pods_IIDadata_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IIDadata_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ F5AC60CC2C67A96800ECCF4B /* IIDadata-Test.swiftpm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "IIDadata-Test.swiftpm"; sourceTree = ""; };
FA9B312D3F353E5C59D2145A /* Pods-IIDadata_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IIDadata_Example.release.xcconfig"; path = "Target Support Files/Pods-IIDadata_Example/Pods-IIDadata_Example.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -78,6 +79,7 @@
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
+ F5AC60CC2C67A96800ECCF4B /* IIDadata-Test.swiftpm */,
607FACF51AFB993E008FA782 /* Podspec Metadata */,
607FACD21AFB9204008FA782 /* Example for IIDadata */,
607FACE81AFB9204008FA782 /* Tests */,
@@ -221,12 +223,12 @@
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
- DevelopmentTeam = 96ZSYGS638;
+ DevelopmentTeam = UB936SP78M;
LastSwiftMigration = 1140;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
- DevelopmentTeam = 96ZSYGS638;
+ DevelopmentTeam = UB936SP78M;
LastSwiftMigration = 1140;
TestTargetID = 607FACCF1AFB9204008FA782;
};
@@ -497,7 +499,7 @@
baseConfigurationReference = 27FEB4AC47CF1BA7638536C4 /* Pods-IIDadata_Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- DEVELOPMENT_TEAM = 96ZSYGS638;
+ DEVELOPMENT_TEAM = UB936SP78M;
INFOPLIST_FILE = IIDadata/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
@@ -513,7 +515,7 @@
baseConfigurationReference = FA9B312D3F353E5C59D2145A /* Pods-IIDadata_Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- DEVELOPMENT_TEAM = 96ZSYGS638;
+ DEVELOPMENT_TEAM = UB936SP78M;
INFOPLIST_FILE = IIDadata/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
@@ -528,7 +530,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 2ABC4B8C1F30EA9DF66C63C8 /* Pods-IIDadata_Tests.debug.xcconfig */;
buildSettings = {
- DEVELOPMENT_TEAM = 96ZSYGS638;
+ DEVELOPMENT_TEAM = UB936SP78M;
FRAMEWORK_SEARCH_PATHS = (
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
"$(inherited)",
@@ -551,7 +553,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7F635716033EB85D6E45087A /* Pods-IIDadata_Tests.release.xcconfig */;
buildSettings = {
- DEVELOPMENT_TEAM = 96ZSYGS638;
+ DEVELOPMENT_TEAM = UB936SP78M;
FRAMEWORK_SEARCH_PATHS = (
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
"$(inherited)",
diff --git a/Example/IIDadata/Info.plist b/Example/IIDadata/Info.plist
index 6c48029..e937298 100644
--- a/Example/IIDadata/Info.plist
+++ b/Example/IIDadata/Info.plist
@@ -22,6 +22,16 @@
1
LSRequiresIPhoneOS
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+ NSExceptionDomains
+
+ suggestions.dadata.ru
+
+
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/Example/Tests/Tests.swift b/Example/Tests/Tests.swift
index 527fd05..fbc0681 100644
--- a/Example/Tests/Tests.swift
+++ b/Example/Tests/Tests.swift
@@ -1,18 +1,9 @@
+@testable import IIDadata
import XCTest
-import IIDadata
-class Tests: XCTestCase {
-
- override func setUp() {
- super.setUp()
- // Put setup code here. This method is called before the invocation of each test method in the class.
- }
-
- override func tearDown() {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
- super.tearDown()
- }
-
+final class Tests: XCTestCase {
+ // Computed Properties
+
// func testExample() {
// // This is an example of a functional test case.
// XCTAssert(true, "Pass")
@@ -24,150 +15,186 @@ class Tests: XCTestCase {
// // Put the code you want to measure the time of here.
// }
// }
- private var apiToken: String {
- // Switch this return to your <# API token #>.
- DadataAPIConstants.token
+ private var apiToken: String {
+ // Switch this return to your <# API token #>.
+ DadataAPIConstants.token
+ }
+
+ // Overridden Functions
+
+ override func setUp() {
+ super.setUp()
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDown() {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ super.tearDown()
+ }
+
+ // Functions
+
+ // FIO Suggestion Tests
+ func testSuggestFIO_BasicQuery() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestFIO("Иванов")
+ XCTAssertGreaterThan(suggestions.count, 0)
+ }
+
+ func testSuggestFIO_FilterByGender() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestFIO("Иванов", gender: .male)
+ XCTAssertGreaterThan(suggestions.count, 0)
+ XCTAssertEqual(suggestions.first?.gender, .male)
+ }
+
+ func testSuggestFIO_FilterByParts() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestFIO("Иванов Иван", parts: [.surname, .firstname])
+ XCTAssertGreaterThan(suggestions.count, 0)
+ XCTAssertEqual(suggestions.first?.surname, "Иванов")
+ XCTAssertEqual(suggestions.first?.firstname, "Иван")
+ }
+
+ #warning("TODO: make separate test cases with assertions.")
+ func testAllCasesJustDoSomething() {
+ DadataSuggestions(apiKey: apiToken)
+ .suggestAddress(
+ "9120b43f-2fae-4838-a144-85e43c2bfb29",
+ queryType: .findByID,
+ resultsCount: 5,
+ language: nil,
+ constraints: ["{\"region\":\"Приморский\"}"],
+ regionPriority: nil,
+ upperScaleLimit: "street",
+ lowerScaleLimit: nil,
+ trimRegionResult: false
+ ) { try? $0.get().suggestions?.forEach { print($0) } }
+
+ var constraint = AddressQueryConstraint()
+ constraint.region = "Приморский"
+ constraint.city = "Владивосток"
+ constraint.country_iso_code = "RU"
+ constraint.region_iso_code = "RU-PRI"
+ try! DadataSuggestions.shared(
+ apiKey: apiToken
+ ).suggestAddress(
+ "г Владивосток, Русский остров, поселок Аякс, д 1",
+ resultsCount: 1,
+ language: .ru,
+ constraints: [constraint],
+ regionPriority: nil,
+ upperScaleLimit: .street,
+ lowerScaleLimit: .house,
+ trimRegionResult: false
+ ) { try? $0.get().suggestions?.forEach { print("\(String(describing: $0.value)) \(String(describing: $0.data?.geoLat)) \(String(describing: $0.data?.geoLon))") } }
+
+ try! DadataSuggestions.shared(
+ apiKey: apiToken
+ ).reverseGeocode(query: "43.026661, 131.8951698",
+ delimeter: ",",
+ resultsCount: 1,
+ language: "ru",
+ searchRadius: 100) { try? $0.get().suggestions?.forEach { print("\(String(describing: $0.value)) \(String(describing: $0.data?.geoLat)) \(String(describing: $0.data?.geoLon))") } }
+
+ let q = AddressSuggestionQuery("9120b43f-2fae-4838-a144-85e43c2bfb29", ofType: .findByID)
+ q.resultsCount = 1
+ q.language = .en
+ DadataSuggestions(apiKey: apiToken)
+ .suggestAddress(q) { try? $0.get().suggestions?.forEach { print($0) } }
+
+ let dadata = try? DadataSuggestions
+ .shared(
+ apiKey: apiToken
+ )
+
+ dadata?.suggestAddressFromFIAS("Тверская обл, Пеновский р-н, деревня Москва") { print((try? $0.get().suggestions) ?? "Nothing") }
+
+ dadata?.suggestAddressFromFIAS("Эрхирик") { print((try? $0.get().suggestions) ?? "Nothing") }
+
+ dadata?.suggestAddress("Эрхирик трактовая 15", resultsCount: 1, constraints: [""]) { r in
+ let v = try? r.get()
+ if let s = v?.suggestions, s.count > 0 {
+ print("\(s[0].value!): LAT \(s[0].data!.geoLat!) @ LON \(s[0].data!.geoLon!)")
+ }
}
-
- #warning("TODO: make separate test cases with assertions.")
- func testAllCasesJustDoSomething(){
- DadataSuggestions(apiKey: apiToken)
- .suggestAddress(
- "9120b43f-2fae-4838-a144-85e43c2bfb29",
- queryType: .findByID,
- resultsCount: 5,
- language: nil,
- constraints: ["{\"region\":\"Приморский\"}"],
- regionPriority: nil,
- upperScaleLimit: "street",
- lowerScaleLimit: nil,
- trimRegionResult: false
- ){ try? $0.get().suggestions?.forEach{ print($0) } }
-
- var constraint = AddressQueryConstraint()
- constraint.region = "Приморский"
- constraint.city = "Владивосток"
- constraint.country_iso_code = "RU"
- constraint.region_iso_code = "RU-PRI"
- try! DadataSuggestions.shared(
- apiKey: apiToken
- ).suggestAddress(
- "г Владивосток, Русский остров, поселок Аякс, д 1",
- resultsCount: 1,
- language: .ru,
- constraints: [constraint],
- regionPriority: nil,
- upperScaleLimit: .street,
- lowerScaleLimit: .house,
- trimRegionResult: false
- ){ try? $0.get().suggestions?.forEach{ print("\(String(describing: $0.value)) \(String(describing: $0.data?.geoLat)) \(String(describing: $0.data?.geoLon))") } }
-
- try! DadataSuggestions.shared(
- apiKey: apiToken
- ).reverseGeocode(query: "43.026661, 131.8951698",
- delimeter: ",",
- resultsCount: 1,
- language:"ru",
- searchRadius: 100){ try? $0.get().suggestions?.forEach{ print("\(String(describing: $0.value)) \(String(describing: $0.data?.geoLat)) \(String(describing: $0.data?.geoLon))") } }
-
-
-
- let q = AddressSuggestionQuery("9120b43f-2fae-4838-a144-85e43c2bfb29", ofType: .findByID)
- q.resultsCount = 1
- q.language = .en
- DadataSuggestions(apiKey: apiToken)
- .suggestAddress(q){ try? $0.get().suggestions?.forEach{ print($0) } }
-
- let dadata = try? DadataSuggestions
- .shared(
- apiKey: apiToken
- )
-
- dadata?.suggestAddressFromFIAS("Тверская обл, Пеновский р-н, деревня Москва"){ print( (try? $0.get().suggestions) ?? "Nothing" ) }
-
- dadata?.suggestAddressFromFIAS("Эрхирик"){ print( (try? $0.get().suggestions) ?? "Nothing" ) }
-
- dadata?.suggestAddress("Эрхирик трактовая 15", resultsCount: 1, constraints: [""]){r in
- let v = try? r.get()
- if let s = v?.suggestions, s.count > 0{
- print("\(s[0].value!): LAT \(s[0].data!.geoLat!) @ LON \(s[0].data!.geoLon!)")
- }
- }
-
- dadata?.suggestByKLADRFIAS("9120b43f-2fae-4838-a144-85e43c2bfb29"){ print( (try? $0.get().suggestions) ?? "Nothing" ) }
-
- try? dadata?.reverseGeocode(query: "52.2620898, 104.3203629",
- delimeter: ",",
- resultsCount: 1,
- language:"ru",
- searchRadius: 100){ r in
- let v = try? r.get()
- if let s = v?.suggestions, s.count > 0{
- print("\(s[0].value!): LAT#\(s[0].data!.geoLat!) @ LON#\(s[0].data!.geoLon!)")
- }
- }
-
- dadata?.reverseGeocode(latitude: 51.5346,
- longitude: 107.4937,
- resultsCount: 1,
- language: .ru,
- searchRadius: 1000){ r in
- let v = try? r.get()
- if let s = v?.suggestions, s.count > 0{
- print("\(s[0].value!): LAT#\(s[0].data!.geoLat!) @ LON#\(s[0].data!.geoLon!)")
- }
- }
-
-
- dadata?.suggestAddress(
- "Пенза московская 1",
- resultsCount: 1,
+
+ dadata?.suggestByKLADRFIAS("9120b43f-2fae-4838-a144-85e43c2bfb29") { print((try? $0.get().suggestions) ?? "Nothing") }
+
+ try? dadata?.reverseGeocode(query: "52.2620898, 104.3203629",
+ delimeter: ",",
+ resultsCount: 1,
+ language: "ru",
+ searchRadius: 100)
+ { r in
+ let v = try? r.get()
+ if let s = v?.suggestions, s.count > 0 {
+ print("\(s[0].value!): LAT#\(s[0].data!.geoLat!) @ LON#\(s[0].data!.geoLon!)")
+ }
+ }
+
+ dadata?.reverseGeocode(latitude: 51.5346,
+ longitude: 107.4937,
+ resultsCount: 1,
+ language: .ru,
+ searchRadius: 1000)
+ { r in
+ let v = try? r.get()
+ if let s = v?.suggestions, s.count > 0 {
+ print("\(s[0].value!): LAT#\(s[0].data!.geoLat!) @ LON#\(s[0].data!.geoLon!)")
+ }
+ }
+
+ dadata?.suggestAddress(
+ "Пенза московская 1",
+ resultsCount: 1,
+ language: "en",
+ completion: { r in
+ switch r {
+ case let .success(v):
+ print(v.suggestions?[0].value as Any)
+ try! dadata?.reverseGeocode(
+ query: "\(v.suggestions![0].data!.geoLat!), \(v.suggestions![0].data!.geoLon!)",
language: "en",
- completion: { r in
- switch r{
- case .success(let v):
- print(v.suggestions?[0].value as Any)
- try! dadata?.reverseGeocode(
- query: "\(v.suggestions![0].data!.geoLat!), \(v.suggestions![0].data!.geoLon!)",
- language: "en",
- searchRadius: 100){ r in
- switch r {
- case .success(let v):
- print(v.suggestions?[0].unrestrictedValue as Any)
- return
- case .failure(let e):
- print(e)
- return
- }
- }
- return
- case .failure(let e):
- print(e)
- return
- }
- }
- )
-
- try! dadata?.reverseGeocode(query: "52.2620898, 104.3203629",
- delimeter: ",",
- resultsCount: 1,
- language:"ru",
- searchRadius: 100){ r in
+ searchRadius: 100
+ ) { r in
switch r {
- case .success(let v):
- print(v)
- return
- case .failure(let e):
- print(e)
- return
+ case let .success(v):
+ print(v.suggestions?[0].unrestrictedValue as Any)
+ return
+ case let .failure(e):
+ print(e)
+ return
}
+ }
+ return
+ case let .failure(e):
+ print(e)
+ return
}
-
- do{
- _ = try DadataSuggestions(apiKey: "some-garbage-token", checkWithTimeout: 15)
- } catch let e {
- print(e)
- }
+ }
+ )
+
+ try! dadata?.reverseGeocode(query: "52.2620898, 104.3203629",
+ delimeter: ",",
+ resultsCount: 1,
+ language: "ru",
+ searchRadius: 100)
+ { r in
+ switch r {
+ case let .success(v):
+ print(v)
+ return
+ case let .failure(e):
+ print(e)
+ return
+ }
+ }
+
+ do {
+ _ = try DadataSuggestions(apiKey: "some-garbage-token", checkWithTimeout: 15)
+ } catch let e {
+ print(e)
}
-
+ }
}
diff --git a/IIDadata/Example/ContentView.swift b/IIDadata/Example/ContentView.swift
new file mode 100644
index 0000000..f7f20c3
--- /dev/null
+++ b/IIDadata/Example/ContentView.swift
@@ -0,0 +1,63 @@
+//
+// ContentView.swift
+// IIDadata
+//
+// Created by NSFuntik on 11.08.2024.
+//
+
+import IIDadata
+import IIDadataUI
+import SwiftUI
+
+// MARK: - IIDadataDemo
+
+@available(iOS 15.0, *)
+struct IIDadataDemo: View {
+ // Properties
+
+ @State var text = "Миха"
+ @State var suggestions: [FioSuggestion]? {
+ willSet {
+ debugPrint(suggestions ?? [], separator: "\n ● ")
+ }
+ }
+ @State var isPresented = false
+ let apiKey: String // = ProcessInfo.processInfo.environment["IIDadataAPIToken"]
+
+ // Lifecycle
+
+ init() {
+ apiKey = ProcessInfo.processInfo.environment["IIDadataAPIToken"] ?? ""
+
+ _suggestions = State(initialValue: [])
+ }
+
+ // Content
+
+ var body: some View {
+ TextField("ФИО", text: $text, prompt: Text("ФИО"))
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding()
+ .font(.body)
+ .withDadataSuggestions(
+ apiKey: apiKey,
+ input: $text,
+ suggestions: $suggestions,
+ textfieldHeight: 44
+ ) { s in
+ debugPrint(s)
+ text = s.value
+ if suggestions?.count == 1 {
+ isPresented = false
+ }
+ }
+ }
+}
+
+// MARK: - PreviewProvider
+@available(iOS 15.0, *)
+struct IIDadataDemo_Previews: PreviewProvider {
+ static var previews: some View {
+ IIDadataDemo()
+ }
+}
diff --git a/IIDadata/Sources/Constants.swift b/IIDadata/Sources/Constants.swift
deleted file mode 100644
index 3a22ae7..0000000
--- a/IIDadata/Sources/Constants.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// Constants.swift
-// IIDadata
-//
-// Created by Yachin Ilya on 11.05.2020.
-//
-
-import Foundation
-
-struct Constants {
- static let suggestionsAPIURL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/"
- static let addressEndpoint = AddressQueryType.address.rawValue
- static let addressFIASOnlyEndpoint = AddressQueryType.fiasOnly.rawValue
- static let addressByIDEndpoint = AddressQueryType.findByID.rawValue
- static let revGeocodeEndpoint = "geolocate/address"
- static let infoPlistTokenKey = "IIDadataAPIToken"
-}
-
-///API endpoints for different request types.
-public enum AddressQueryType: String {
- case address = "suggest/address"
- case fiasOnly = "suggest/fias"
- case findByID = "findById/address"
-}
-
-///Language of response.
-public enum QueryResultLanguage: String, Encodable {
- case ru = "ru"
- case en = "en"
-}
diff --git a/IIDadata/Sources/DadataQueryProtocol.swift b/IIDadata/Sources/DadataQueryProtocol.swift
deleted file mode 100644
index b77ebfb..0000000
--- a/IIDadata/Sources/DadataQueryProtocol.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// DadataQueryProtocol.swift
-// IIDadata
-//
-// Created by Yachin Ilya on 12.05.2020.
-//
-
-import Foundation
-
-protocol DadataQueryProtocol {
- func queryEndpoint() -> String
- func toJSON() throws -> Data
-}
-
-
diff --git a/IIDadata/Sources/DadataSuggestions.swift b/IIDadata/Sources/DadataSuggestions.swift
deleted file mode 100644
index ea11b8d..0000000
--- a/IIDadata/Sources/DadataSuggestions.swift
+++ /dev/null
@@ -1,350 +0,0 @@
-import Foundation
-
-///DadataSuggestions performs all the interactions with Dadata API.
-public class DadataSuggestions {
- private let apiKey: String
- private var suggestionsAPIURL: URL
- private static var sharedInstance: DadataSuggestions?
-
- ///New instance of DadataSuggestions.
- ///
- ///
- ///Required API key is read from Info.plist. Each init creates new instance using same token.
- ///If DadataSuggestions is used havily consider `DadataSuggestions.shared()` instead.
- ///- Precondition: Token set with "IIDadataAPIToken" key in Info.plist.
- ///- Throws: Call may throw if there isn't a value for key "IIDadataAPIToken" set in Info.plist.
- public convenience init() throws {
- let key = try DadataSuggestions.readAPIKeyFromPlist()
- self.init(apiKey: key)
- }
-
- ///This init checks connectivity once the class instance is set.
- ///
- ///This init should not be called on main thread as it may take up long time as it makes request to server in a blocking manner.
- ///Throws if connection is impossible or request is timed out.
- ///```
- ///DispatchQueue.global(qos: .background).async {
- /// let dadata = try DadataSuggestions(apiKey: "<# Dadata API token #>", checkWithTimeout: 15)
- ///}
- ///```
- ///- Parameter apiKey: Dadata API token. Check it in account settings at dadata.ru.
- ///- Parameter checkWithTimeout: Time in seconds to wait for response.
- ///
- ///- Throws: May throw on connectivity problems, missing or wrong API token, limits exeeded, wrong endpoint.
- ///May throw if request is timed out.
- public convenience init(apiKey: String, checkWithTimeout timeout: Int) throws {
- self.init(apiKey: apiKey)
- try checkAPIConnectivity(timeout: timeout)
- }
-
- ///New instance of DadataSuggestions.
- ///- Parameter apiKey: Dadata API token. Check it in account settings at dadata.ru.
- public convenience required init(apiKey: String) {
- self.init(apiKey: apiKey, url: Constants.suggestionsAPIURL)
- }
-
- private init(apiKey: String, url: String) {
- self.apiKey = apiKey
- self.suggestionsAPIURL = URL(string: url)!
- }
-
- ///Get shared instance of DadataSuggestions class.
- ///
- ///Call may throw if neither apiKey parameter is provided
- ///nor a value for key "IIDadataAPIToken" is set in Info.plist
- ///whenever shared instance weren't instantiated earlier.
- ///If another apiKey provided new shared instance of DadataSuggestions recreated with the provided API token.
- ///- Parameter apiKey: Dadata API token. Check it in account settings at dadata.ru.
- public static func shared(apiKey: String? = nil) throws -> DadataSuggestions {
- if let instance = sharedInstance, instance.apiKey == apiKey || apiKey == nil { return instance }
-
-
- if let key = apiKey { sharedInstance = DadataSuggestions(apiKey: key); return sharedInstance! }
-
- let key = try readAPIKeyFromPlist()
- sharedInstance = DadataSuggestions(apiKey: key)
- return sharedInstance!
- }
-
- private static func readAPIKeyFromPlist() throws -> String {
- var dictionary: NSDictionary?
- if let path = Bundle.main.path(forResource: "Info", ofType: "plist") {
- dictionary = NSDictionary(contentsOfFile: path)
- }
- guard let key = dictionary?.value(forKey: Constants.infoPlistTokenKey) as? String else {
- throw NSError(domain: "Dadata API key missing in Info.plist", code: 1, userInfo: nil )
- }
- return key
- }
-
- private func checkAPIConnectivity(timeout: Int) throws {
- var request = createRequest(url:suggestionsAPIURL.appendingPathComponent(Constants.addressEndpoint))
- request.timeoutInterval = TimeInterval(timeout)
-
- let semaphore = DispatchSemaphore.init(value: 0)
- var errorValue: Error?
-
- let session = URLSession.shared
- session.dataTask(with: request){[weak self] data,response,error in
- defer { semaphore.signal() }
- if error != nil { errorValue = error; return }
- if let response = (response as? HTTPURLResponse), (200...299 ~= response.statusCode) == false {
- errorValue = self?.nonOKResponseToError(response: response, body: data)
- return
- }
- }.resume()
-
- semaphore.wait()
-
- if let e = errorValue{
- throw e
- }
- }
-
- private func createRequest(url: URL)->URLRequest{
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
- request.addValue("Token " + apiKey, forHTTPHeaderField: "Authorization")
- return request
- }
-
- private func nonOKResponseToError(response: HTTPURLResponse, body data: Data?)->Error{
- let code = response.statusCode
- var info: [String: Any] = [:]
- response.allHeaderFields.forEach{ if let k = $0.key as? String { info[k] = $0.value } }
- if let data = data {
- let object = try? JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any]
- object?.forEach{ if let k = $0.key as? String { info[k] = $0.value } }
- }
- return NSError(domain: "HTTP Status \(HTTPURLResponse.localizedString(forStatusCode: code))", code: code, userInfo: info)
- }
-
- ///Basic address suggestions request with only rquired data.
- ///
- ///- Parameter query: Query string to send to API. String of a free-form e.g. address part.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of address suggestion query.
- public func suggestAddress(_ query: String, completion: @escaping (_ result: Result)->Void){
- suggestAddress(AddressSuggestionQuery(query), completion: completion)
- }
-
- ///Address suggestions request.
- ///
- ///Limitations, filters and constraints may be applied to query.
- ///
- ///- Parameter query: Query string to send to API. String of a free-form e.g. address part.
- ///- Parameter queryType: Lets select whether the request type. There are 3 query types available:
- ///`address` — standart address suggestion query;
- ///`fiasOnly` — query to only search in FIAS database: less matches, state provided address data only;
- ///`findByID` — takes KLADR or FIAS ID as a query parameter to lookup additional data.
- ///- Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
- ///including latitude and longitude. `20` is a maximum value.
- ///- Parameter language: Suggested results may be in Russian or English.
- ///- Parameter constraints: List of `AddressQueryConstraint` objects to filter results.
- ///- Parameter regionPriority: List of RegionPriority objects to prefer in lookup.
- ///- Parameter upperScaleLimit: Bigger `ScaleLevel` object in pair of scale limits.
- ///- Parameter lowerScaleLimit: Smaller `ScaleLevel` object in pair of scale limits.
- ///- Parameter trimRegionResult: Remove region and city names from suggestion top level.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of address suggestion query.
- public func suggestAddress(_ query: String,
- queryType: AddressQueryType = .address,
- resultsCount: Int? = 10,
- language: QueryResultLanguage? = nil,
- constraints: [AddressQueryConstraint]? = nil,
- regionPriority: [RegionPriority]? = nil,
- upperScaleLimit: ScaleLevel? = nil,
- lowerScaleLimit: ScaleLevel? = nil,
- trimRegionResult: Bool = false,
- completion: @escaping (_ result: Result)->Void){
-
- let suggestionQuery = AddressSuggestionQuery(query, ofType: queryType)
-
- suggestionQuery.resultsCount = resultsCount
- suggestionQuery.language = language
- suggestionQuery.constraints = constraints
- suggestionQuery.regionPriority = regionPriority
- suggestionQuery.upperScaleLimit = upperScaleLimit != nil ? ScaleBound(value: upperScaleLimit) : nil
- suggestionQuery.lowerScaleLimit = upperScaleLimit != nil ? ScaleBound(value: lowerScaleLimit) : nil
- suggestionQuery.trimRegionResult = trimRegionResult
-
- suggestAddress(suggestionQuery, completion: completion)
- }
-
- ///Address suggestions request.
- ///
- ///Allows to pass most of arguments as a strings converting to internally used classes.
- ///
- ///- Parameter query: Query string to send to API. String of a free-form e.g. address part.
- ///- Parameter queryType: Lets select whether the request type. There are 3 query types available:
- ///`address` — standart address suggestion query;
- ///`fiasOnly` — query to only search in FIAS database: less matches, state provided address data only;
- ///`findByID` — takes KLADR or FIAS ID as a query parameter to lookup additional data.
- ///- Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
- ///including latitude and longitude. `20` is a maximum value.
- ///- Parameter language: Suggested results in "ru" — Russian or "en" — English.
- ///- Parameter constraints: Literal JSON string formated according to
- ///[Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=204669108).
- ///- Parameter regionPriority: List of regions' KLADR IDs to prefer in lookup as shown in
- ///[Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=285343795).
- ///- Parameter upperScaleLimit: Bigger sized object in pair of scale limits.
- ///- Parameter lowerScaleLimit: Smaller sized object in pair of scale limits. Both can take following values:
- ///`country` — Страна,
- ///`region` — Регион,
- ///`area` — Район,
- ///`city` — Город,
- ///`settlement` — Населенный пункт,
- ///`street` — Улица,
- ///`house` — Дом,
- ///`country` — Страна,
- ///- Parameter trimRegionResult: Remove region and city names from suggestion top level.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of address suggestion query.
- public func suggestAddress(_ query: String,
- queryType: AddressQueryType = .address,
- resultsCount: Int? = 10,
- language: String? = nil,
- constraints: [String]? = nil,
- regionPriority: [String]? = nil,
- upperScaleLimit: String? = nil,
- lowerScaleLimit: String? = nil,
- trimRegionResult: Bool = false,
- completion: @escaping (_ result: Result)->Void){
-
- let queryConstraints: [AddressQueryConstraint]? = constraints?.compactMap{
- if let data = $0.data(using: .utf8) {
- return try? JSONDecoder().decode(AddressQueryConstraint.self, from: data )
- }
- return nil
- }
- let prefferedRegions: [RegionPriority]? = regionPriority?.compactMap{ RegionPriority(kladr_id: $0) }
-
- suggestAddress(query,
- queryType: queryType,
- resultsCount: resultsCount,
- language: QueryResultLanguage(rawValue: language ?? "ru"),
- constraints: queryConstraints,
- regionPriority: prefferedRegions,
- upperScaleLimit: ScaleLevel(rawValue: upperScaleLimit ?? "*"),
- lowerScaleLimit: ScaleLevel(rawValue: lowerScaleLimit ?? "*"),
- trimRegionResult: trimRegionResult,
- completion: completion)
- }
-
- ///Basic address suggestions request to only search in FIAS database: less matches, state provided address data only.
- ///
- ///- Parameter query: Query string to send to API. String of a free-form e.g. address part.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of address suggestion query.
- public func suggestAddressFromFIAS(_ query: String, completion: @escaping (_ result: Result)->Void){
- suggestAddress(AddressSuggestionQuery(query, ofType: .fiasOnly), completion: completion)
- }
-
- ///Basic address suggestions request takes KLADR or FIAS ID as a query parameter to lookup additional data.
- ///
- ///- Parameter query: KLADR or FIAS ID.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of address suggestion query.
- public func suggestByKLADRFIAS(_ query: String, completion: @escaping (_ result: Result)->Void){
- suggestAddress(AddressSuggestionQuery(query, ofType: .findByID), completion: completion)
- }
-
- ///Address suggestion request with custom `AddressSuggestionQuery`.
- ///
- ///- Parameter query: Query object.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of address suggestion query.
- public func suggestAddress(_ query: AddressSuggestionQuery, completion: @escaping (_ result: Result)->Void){
- fetchResponse(withQuery: query, completionHandler: completion)
- }
-
- ///Reverse Geocode request with latitude and longitude as a single string.
- ///
- ///- Throws: May throw if query is malformed.
- ///
- ///- Parameter query: Latitude and longitude as a string. Should have single character separator.
- ///- Parameter delimeter: Character to separate latitude and longitude. Defaults to '`,`'
- ///- Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
- ///including latitude and longitude. `20` is a maximum value.
- ///- Parameter language: Suggested results in "ru" — Russian or "en" — English.
- ///- Parameter searchRadius: Radius to suggest objects nearest to coordinates point.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of reverse geocode query.
- public func reverseGeocode(query: String,
- delimeter: Character = ",",
- resultsCount: Int? = 10,
- language: String? = "ru",
- searchRadius: Int? = nil,
- completion: @escaping (_ result: Result)->Void) throws {
-
- let geoquery = try ReverseGeocodeQuery(query: query, delimeter: delimeter)
- geoquery.resultsCount = resultsCount
- geoquery.language = QueryResultLanguage(rawValue: language ?? "ru")
- geoquery.searchRadius = searchRadius
-
- reverseGeocode(geoquery, completion: completion)
- }
-
- ///Reverse Geocode request with latitude and longitude as a single string.
- ///
- ///- Parameter latitude: Latitude.
- ///- Parameter longitude: Longitude.
- ///- Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
- ///including latitude and longitude. `20` is a maximum value.
- ///- Parameter language: Suggested results may be in Russian or English.
- ///- Parameter searchRadius: Radius to suggest objects nearest to coordinates point.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of reverse geocode query.
- public func reverseGeocode(latitude: Double,
- longitude: Double,
- resultsCount: Int? = 10,
- language: QueryResultLanguage? = nil,
- searchRadius: Int? = nil,
- completion: @escaping (_ result: Result)->Void){
- let geoquery = ReverseGeocodeQuery(latitude: latitude, longitude: longitude)
- geoquery.resultsCount = resultsCount
- geoquery.language = language
- geoquery.searchRadius = searchRadius
-
- fetchResponse(withQuery: geoquery, completionHandler: completion)
- }
-
- ///Reverse geocode request with custom `ReverseGeocodeQuery`.
- ///
- ///- Parameter query: Query object.
- ///- Parameter completion: Result handler.
- ///- Parameter result: result of reverse geocode query.
- public func reverseGeocode(_ query: ReverseGeocodeQuery, completion: @escaping (_ result: Result)->Void){
- fetchResponse(withQuery: query, completionHandler: completion)
- }
-
- private func fetchResponse(withQuery query: DadataQueryProtocol, completionHandler completion: @escaping (Result)->Void) where T: Decodable {
- var request = createRequest(url:suggestionsAPIURL.appendingPathComponent(query.queryEndpoint()))
- request.httpBody = try? query.toJSON()
- let session = URLSession.shared
- session.dataTask(with: request){data,response,error in
-
- if let error = error {
- completion(.failure(error))
- return
- }
- if let response = (response as? HTTPURLResponse), (200...299 ~= response.statusCode) == false {
- completion(.failure(NSError(domain: "Dadata HTTP response", code: response.statusCode, userInfo: ["description": response.description])))
- return
- }
- guard let data = data else {
- completion(.failure(NSError(domain: "Dadata HTTP response", code: -1, userInfo: ["description": "missing data in response"])))
- return
- }
- do {
- let result = try JSONDecoder().decode(T.self, from: data)
- completion(.success(result))
- return
- } catch let e {
- completion(.failure(e))
- return
- }
- }.resume()
- }
-}
diff --git a/IIDadata/Sources/Extension/KeyedDecodingContainer+decodeJSONNumber.swift b/IIDadata/Sources/Extension/KeyedDecodingContainer+decodeJSONNumber.swift
deleted file mode 100644
index 9b798db..0000000
--- a/IIDadata/Sources/Extension/KeyedDecodingContainer+decodeJSONNumber.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-// KeyedDecodingContainer+decodeJSONNumber.swift
-// IIDadata
-//
-// Created by Yachin Ilya on 23.07.2020.
-//
-
-import Foundation
-
-extension KeyedDecodingContainer {
- /// Forces integers and floating point numbers to optional String type.
- /// Helpful when JSON response may include inconsistency in number fields.
- /// - parameter key: CodingKey of Decodable object CodingKeys to lookup.
- func decodeJSONNumber(forKey key: CodingKey) -> String? {
- if let v = try? decode(String.self, forKey: key as! K) {
- return v
- }
- if let v = try? decode(Int.self, forKey: key as! K) {
- return "\(v)"
- }
- if let v = try? decode(Double.self, forKey: key as! K) {
- return "\(v)"
- }
- return nil
- }
-}
diff --git a/IIDadata/Sources/Model/AddressSuggestionQuery.swift b/IIDadata/Sources/Model/AddressSuggestionQuery.swift
deleted file mode 100644
index fbdb97c..0000000
--- a/IIDadata/Sources/Model/AddressSuggestionQuery.swift
+++ /dev/null
@@ -1,157 +0,0 @@
-//
-// AddressSuggestionQuery.swift
-// IIDadata
-//
-// Created by Yachin Ilya on 12.05.2020.
-//
-
-import Foundation
-
-///AddressSuggestionQuery represents an serializable object used to perform certain queries.
-public class AddressSuggestionQuery: Encodable, DadataQueryProtocol{
- let query: String
- let queryType: AddressQueryType
- public var resultsCount: Int? = 10
- public var language: QueryResultLanguage?
- public var constraints: [AddressQueryConstraint]?
- public var regionPriority: [RegionPriority]?
- public var upperScaleLimit: ScaleBound?
- public var lowerScaleLimit: ScaleBound?
- public var trimRegionResult: Bool = false
-
- ///New instance of AddressSuggestionQuery defaulting to simple address suggestions request.
- ///- Parameter query: Query string to be sent to API.
- public convenience init(_ query: String){
- self.init(query, ofType: .address)
- }
-
- ///New instance of AddressSuggestionQuery.
- ///- Parameter query: Query string to be sent to API.
- ///- Parameter ofType: Type of request to send to API.
- ///It could be of type
- ///`address` — standart address suggestion query;
- ///`fiasOnly` — query to only search in FIAS database: less matches, state provided address data only;
- ///`findByID` — takes KLADR or FIAS ID as a query parameter to lookup additional data.
- public required init(_ query: String, ofType type: AddressQueryType){
- self.query = query
- self.queryType = type
- }
-
- enum CodingKeys: String, CodingKey {
- case query
- case resultsCount = "count"
- case language
- case constraints = "locations"
- case regionPriority = "locations_boost"
- case upperScaleLimit = "from_bound"
- case lowerScaleLimit = "to_bound"
- case trimRegionResult = "restrict_value"
- }
-
- ///Serializes AddressSuggestionQuery to send over the wire.
- func toJSON() throws -> Data {
- if constraints?.isEmpty ?? false { constraints = nil }
- if regionPriority?.isEmpty ?? false { regionPriority = nil }
- if let upper = upperScaleLimit,
- let lower = lowerScaleLimit,
- upper.value == nil || lower.value == nil {
- upperScaleLimit = nil
- lowerScaleLimit = nil
- }
- return try JSONEncoder().encode(self)
- }
-
- ///Returns an API endpoint for different request types:
- ///`address` — "suggest/address"
- ///`fiasOnly` — "suggest/fias"
- ///`findByID` — "findById/address"
- func queryEndpoint() -> String { return queryType.rawValue }
-}
-
-///Levels of `from_bound` and `to_bound` according to
-///[Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=285343795).
-public enum ScaleLevel: String, Encodable {
- case country = "country"
- case region = "region"
- case area = "area"
- case city = "city"
- case settlement = "settlement"
- case street = "street"
- case house = "house"
-}
-
-///AddressQueryConstraint used to limit search results according to
-///[Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=204669108).
-public struct AddressQueryConstraint: Codable{
- public var region : String?
- public var city : String?
- public var street_type_full : String?
- public var settlement_type_full : String?
- public var city_district_type_full : String?
- public var city_type_full : String?
- public var area_type_full : String?
- public var region_type_full : String?
- public var country : String?
- public var country_iso_code : String?
- public var region_iso_code : String?
- public var kladr_id : String?
- public var region_fias_id : String?
- public var area_fias_id : String?
- public var city_fias_id : String?
- public var settlement_fias_id : String?
- public var street_fias_id : String?
-
- public init(
- region: String? = nil,
- city: String? = nil,
- street_type_full: String? = nil,
- settlement_type_full: String? = nil,
- city_district_type_full: String? = nil,
- city_type_full: String? = nil,
- area_type_full: String? = nil,
- region_type_full: String? = nil,
- country: String? = nil,
- country_iso_code: String? = nil,
- region_iso_code: String? = nil,
- kladr_id: String? = nil,
- region_fias_id: String? = nil,
- area_fias_id: String? = nil,
- city_fias_id: String? = nil,
- settlement_fias_id: String? = nil,
- street_fias_id: String? = nil
- ) {
- self.region = region
- self.city = city
- self.street_type_full = street_type_full
- self.settlement_type_full = settlement_type_full
- self.city_district_type_full = city_district_type_full
- self.city_type_full = city_type_full
- self.area_type_full = area_type_full
- self.region_type_full = region_type_full
- self.country = country
- self.country_iso_code = country_iso_code
- self.region_iso_code = region_iso_code
- self.kladr_id = kladr_id
- self.region_fias_id = region_fias_id
- self.area_fias_id = area_fias_id
- self.city_fias_id = city_fias_id
- self.settlement_fias_id = settlement_fias_id
- self.street_fias_id = street_fias_id
- }
-}
-
-///Helps prioritize specified region in search results by KLADR ID.
-public struct RegionPriority: Encodable{
- public var kladr_id: String?
-
- public init(kladr_id: String?){ self.kladr_id = kladr_id }
-}
-
-///ScaleBound holds a value for `from_bound` and `to_bound` as a ScaleLevel.
-///See
-///[Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=285343795) for API reference.
-public struct ScaleBound: Encodable{
- public var value: ScaleLevel?
-
- public init(value: ScaleLevel?){ self.value = value }
-}
diff --git a/IIDadata/Sources/Model/AddressSuggestionResponse.swift b/IIDadata/Sources/Model/AddressSuggestionResponse.swift
deleted file mode 100644
index 4e6da1c..0000000
--- a/IIDadata/Sources/Model/AddressSuggestionResponse.swift
+++ /dev/null
@@ -1,363 +0,0 @@
-//
-// Models Generated using http://www.jsoncafe.com/
-// Created on May 11, 2020
-
-import Foundation
-
-///AddressSuggestionResponse represents a deserializable object used to hold API response.
-public struct AddressSuggestionResponse : Decodable {
- public let suggestions : [AddressSuggestions]?
-}
-
-///Every single suggestion is represented as AddressSuggestions.
-public struct AddressSuggestions : Decodable {
- ///Address in short format.
- public let value : String?
- ///All the data returned in response to suggestion query.
- public let data : AddressSuggestionData?
- ///Address in long format with region.
- public let unrestrictedValue : String?
-
- enum CodingKeys: String, CodingKey {
- case value
- case data
- case unrestrictedValue = "unrestricted_value"
- }
-}
-
-///All the data returned in response to suggestion query.
-public struct AddressSuggestionData : Decodable {
- public let area : String?
- public let areaFiasId : String?
- public let areaKladrId : String?
- public let areaType : String?
- public let areaTypeFull : String?
- public let areaWithType : String?
- public let beltwayDistance : String?
- public let beltwayHit : String?
- public let block : String?
- public let blockType : String?
- public let blockTypeFull : String?
- public let building : String?
- public let buildingType : String?
- public let cadastralNumber : String?
- /// - `1` — subregion (district) center
- /// - `2` — region center
- /// - `3` — `1` and `2` combined
- /// - `4` — main subregion (district) in region
- /// - `0` — no status
- public let capitalMarker : String?
- public let city : String?
- public let cityArea : String?
- public let cityDistrict : String?
- public let cityDistrictFiasId : String?
- public let cityDistrictKladrId : String?
- public let cityDistrictType : String?
- public let cityDistrictTypeFull : String?
- public let cityDistrictWithType : String?
- public let cityFiasId : String?
- public let cityKladrId : String?
- public let cityType : String?
- public let cityTypeFull : String?
- public let cityWithType : String?
- public let country : String?
- public let countryIsoCode : String?
- public let federalDistrict : String?
- /// FIAS actuality
- /// - `0` — actual
- /// - `1–50` — renamed
- /// - `51` — changed
- /// - `99` — removed
- public let fiasActualityState : String?
- /// Structure of FIAS code (СС+РРР+ГГГ+ППП+СССС+УУУУ+ДДДД)
- public let fiasCode : String?
- public let fiasId : String?
- /// FIAS address precision
- /// - `0` — country
- /// - `1` — region
- /// - `3` — subregion (district of region)
- /// - `4` — city
- /// - `5` — city district
- /// - `6` — locality, neighbourhood, settlement etc.
- /// - `7` — street
- /// - `8` — building
- /// - `9` — flat / apartment
- /// - `65` — city plan unit
- /// - `-1` — empty or abroad
- public let fiasLevel : String?
- public let flat : String?
- public let flatFiasId : String?
- public let flatArea : String?
- public let flatPrice : String?
- public let flatType : String?
- public let flatTypeFull : String?
- public let geoLat : String?
- public let geoLon : String?
- public let geonameId : String?
- public let historyValues : [String]?
- public let house : String?
- public let houseFiasId : String?
- public let houseKladrId : String?
- public let houseType : String?
- public let houseTypeFull : String?
- public let kladrId : String?
- public let metro : [Metro]?
- public let okato : String?
- public let oktmo : String?
- public let planningStructure : String?
- public let planningStructureFiasId : String?
- public let planningStructureKladrId : String?
- public let planningStructureType : String?
- public let planningStructureTypeFull : String?
- public let planningStructureWithType : String?
- public let postalBox : String?
- public let postalCode : String?
- public let qc : String?
- public let qcComplete : String?
- /// Coordinates precision
- /// - `0` — precise
- /// - `1` — nearest building
- /// - `2` — nearest street
- /// - `3` — city district, locality, neighbourhood, settlement etc.
- /// - `4` — city
- /// - `5` — failed to determine coordinates
- public let qcGeo : String?
- public let qcHouse : String?
- public let region : String?
- public let regionFiasId : String?
- public let regionIsoCode : String?
- public let regionKladrId : String?
- public let regionType : String?
- public let regionTypeFull : String?
- public let regionWithType : String?
- public let settlement : String?
- public let settlementFiasId : String?
- public let settlementKladrId : String?
- public let settlementType : String?
- public let settlementTypeFull : String?
- public let settlementWithType : String?
- public let source : String?
- public let squareMeterPrice : String?
- public let street : String?
- public let streetFiasId : String?
- public let streetKladrId : String?
- public let streetType : String?
- public let streetTypeFull : String?
- public let streetWithType : String?
- public let taxOffice : String?
- public let taxOfficeLegal : String?
- public let timezone : String?
- public let unparsedParts : String?
-
- enum CodingKeys: String, CodingKey {
- case area = "area"
- case areaFiasId = "area_fias_id"
- case areaKladrId = "area_kladr_id"
- case areaType = "area_type"
- case areaTypeFull = "area_type_full"
- case areaWithType = "area_with_type"
- case beltwayDistance = "beltway_distance"
- case beltwayHit = "beltway_hit"
- case block = "block"
- case blockType = "block_type"
- case blockTypeFull = "block_type_full"
- case building = "building"
- case buildingType = "building_type"
- case cadastralNumber = "cadastral_number"
- case capitalMarker = "capital_marker"
- case city = "city"
- case cityArea = "city_area"
- case cityDistrict = "city_district"
- case cityDistrictFiasId = "city_district_fias_id"
- case cityDistrictKladrId = "city_district_kladr_id"
- case cityDistrictType = "city_district_type"
- case cityDistrictTypeFull = "city_district_type_full"
- case cityDistrictWithType = "city_district_with_type"
- case cityFiasId = "city_fias_id"
- case cityKladrId = "city_kladr_id"
- case cityType = "city_type"
- case cityTypeFull = "city_type_full"
- case cityWithType = "city_with_type"
- case country = "country"
- case countryIsoCode = "country_iso_code"
- case federalDistrict = "federal_district"
- case fiasActualityState = "fias_actuality_state"
- case fiasCode = "fias_code"
- case fiasId = "fias_id"
- case fiasLevel = "fias_level"
- case flat = "flat"
- case flatFiasId = "flat_fias_id"
- case flatArea = "flat_area"
- case flatPrice = "flat_price"
- case flatType = "flat_type"
- case flatTypeFull = "flat_type_full"
- case geoLat = "geo_lat"
- case geoLon = "geo_lon"
- case geonameId = "geoname_id"
- case historyValues = "history_values"
- case house = "house"
- case houseFiasId = "house_fias_id"
- case houseKladrId = "house_kladr_id"
- case houseType = "house_type"
- case houseTypeFull = "house_type_full"
- case kladrId = "kladr_id"
- case metro = "metro"
- case okato = "okato"
- case oktmo = "oktmo"
- case planningStructure = "planning_structure"
- case planningStructureFiasId = "planning_structure_fias_id"
- case planningStructureKladrId = "planning_structure_kladr_id"
- case planningStructureType = "planning_structure_type"
- case planningStructureTypeFull = "planning_structure_type_full"
- case planningStructureWithType = "planning_structure_with_type"
- case postalBox = "postal_box"
- case postalCode = "postal_code"
- case qc = "qc"
- case qcComplete = "qc_complete"
- case qcGeo = "qc_geo"
- case qcHouse = "qc_house"
- case region = "region"
- case regionFiasId = "region_fias_id"
- case regionIsoCode = "region_iso_code"
- case regionKladrId = "region_kladr_id"
- case regionType = "region_type"
- case regionTypeFull = "region_type_full"
- case regionWithType = "region_with_type"
- case settlement = "settlement"
- case settlementFiasId = "settlement_fias_id"
- case settlementKladrId = "settlement_kladr_id"
- case settlementType = "settlement_type"
- case settlementTypeFull = "settlement_type_full"
- case settlementWithType = "settlement_with_type"
- case source = "source"
- case squareMeterPrice = "square_meter_price"
- case street = "street"
- case streetFiasId = "street_fias_id"
- case streetKladrId = "street_kladr_id"
- case streetType = "street_type"
- case streetTypeFull = "street_type_full"
- case streetWithType = "street_with_type"
- case taxOffice = "tax_office"
- case taxOfficeLegal = "tax_office_legal"
- case timezone = "timezone"
- case unparsedParts = "unparsed_parts"
- }
-
- public init(from decoder: Decoder) throws {
- let values = try decoder.container(keyedBy: CodingKeys.self)
- area = try values.decodeIfPresent(String.self, forKey: .area)
- areaFiasId = try values.decodeIfPresent(String.self, forKey: .areaFiasId)
- areaKladrId = try values.decodeIfPresent(String.self, forKey: .areaKladrId)
- areaType = try values.decodeIfPresent(String.self, forKey: .areaType)
- areaTypeFull = try values.decodeIfPresent(String.self, forKey: .areaTypeFull)
- areaWithType = try values.decodeIfPresent(String.self, forKey: .areaWithType)
- beltwayDistance = try values.decodeIfPresent(String.self, forKey: .beltwayDistance)
- beltwayHit = try values.decodeIfPresent(String.self, forKey: .beltwayHit)
- block = try values.decodeIfPresent(String.self, forKey: .block)
- blockType = try values.decodeIfPresent(String.self, forKey: .blockType)
- blockTypeFull = try values.decodeIfPresent(String.self, forKey: .blockTypeFull)
- building = try values.decodeIfPresent(String.self, forKey: .building)
- buildingType = try values.decodeIfPresent(String.self, forKey: .buildingType)
- cadastralNumber = try values.decodeIfPresent(String.self, forKey: .cadastralNumber)
- capitalMarker = try values.decodeIfPresent(String.self, forKey: .capitalMarker)
- city = try values.decodeIfPresent(String.self, forKey: .city)
- cityArea = try values.decodeIfPresent(String.self, forKey: .cityArea)
- cityDistrict = try values.decodeIfPresent(String.self, forKey: .cityDistrict)
- cityDistrictFiasId = try values.decodeIfPresent(String.self, forKey: .cityDistrictFiasId)
- cityDistrictKladrId = try values.decodeIfPresent(String.self, forKey: .cityDistrictKladrId)
- cityDistrictType = try values.decodeIfPresent(String.self, forKey: .cityDistrictType)
- cityDistrictTypeFull = try values.decodeIfPresent(String.self, forKey: .cityDistrictTypeFull)
- cityDistrictWithType = try values.decodeIfPresent(String.self, forKey: .cityDistrictWithType)
- cityFiasId = try values.decodeIfPresent(String.self, forKey: .cityFiasId)
- cityKladrId = try values.decodeIfPresent(String.self, forKey: .cityKladrId)
- cityType = try values.decodeIfPresent(String.self, forKey: .cityType)
- cityTypeFull = try values.decodeIfPresent(String.self, forKey: .cityTypeFull)
- cityWithType = try values.decodeIfPresent(String.self, forKey: .cityWithType)
- country = try values.decodeIfPresent(String.self, forKey: .country)
- countryIsoCode = try values.decodeIfPresent(String.self, forKey: .countryIsoCode)
- federalDistrict = try values.decodeIfPresent(String.self, forKey: .federalDistrict)
- fiasActualityState = try values.decodeIfPresent(String.self, forKey: .fiasActualityState)
- fiasCode = try values.decodeIfPresent(String.self, forKey: .fiasCode)
- fiasId = try values.decodeIfPresent(String.self, forKey: .fiasId)
- fiasLevel = try values.decodeIfPresent(String.self, forKey: .fiasLevel)
- flat = try values.decodeIfPresent(String.self, forKey: .flat)
- flatFiasId = try values.decodeIfPresent(String.self, forKey: .flatFiasId)
- flatArea = try values.decodeIfPresent(String.self, forKey: .flatArea)
- flatPrice = try values.decodeIfPresent(String.self, forKey: .flatPrice)
- flatType = try values.decodeIfPresent(String.self, forKey: .flatType)
- flatTypeFull = try values.decodeIfPresent(String.self, forKey: .flatTypeFull)
- geoLat = try values.decodeIfPresent(String.self, forKey: .geoLat)
- geoLon = try values.decodeIfPresent(String.self, forKey: .geoLon)
- geonameId = try values.decodeIfPresent(String.self, forKey: .geonameId)
- historyValues = try values.decodeIfPresent([String].self, forKey: .historyValues)
- house = try values.decodeIfPresent(String.self, forKey: .house)
- houseFiasId = try values.decodeIfPresent(String.self, forKey: .houseFiasId)
- houseKladrId = try values.decodeIfPresent(String.self, forKey: .houseKladrId)
- houseType = try values.decodeIfPresent(String.self, forKey: .houseType)
- houseTypeFull = try values.decodeIfPresent(String.self, forKey: .houseTypeFull)
- kladrId = try values.decodeIfPresent(String.self, forKey: .kladrId)
- metro = try values.decodeIfPresent([Metro].self, forKey: .metro)
- okato = try values.decodeIfPresent(String.self, forKey: .okato)
- oktmo = try values.decodeIfPresent(String.self, forKey: .oktmo)
- planningStructure = try values.decodeIfPresent(String.self, forKey: .planningStructure)
- planningStructureFiasId = try values.decodeIfPresent(String.self, forKey: .planningStructureFiasId)
- planningStructureKladrId = try values.decodeIfPresent(String.self, forKey: .planningStructureKladrId)
- planningStructureType = try values.decodeIfPresent(String.self, forKey: .planningStructureType)
- planningStructureTypeFull = try values.decodeIfPresent(String.self, forKey: .planningStructureTypeFull)
- planningStructureWithType = try values.decodeIfPresent(String.self, forKey: .planningStructureWithType)
- postalBox = try values.decodeIfPresent(String.self, forKey: .postalBox)
- postalCode = try values.decodeIfPresent(String.self, forKey: .postalCode)
-
- qc = values.decodeJSONNumber(forKey: CodingKeys.qc)
- qcComplete = values.decodeJSONNumber(forKey: CodingKeys.qcComplete)
- qcGeo = values.decodeJSONNumber(forKey: CodingKeys.qcGeo)
- qcHouse = values.decodeJSONNumber(forKey: CodingKeys.qcHouse)
-
- region = try values.decodeIfPresent(String.self, forKey: .region)
- regionFiasId = try values.decodeIfPresent(String.self, forKey: .regionFiasId)
- regionIsoCode = try values.decodeIfPresent(String.self, forKey: .regionIsoCode)
- regionKladrId = try values.decodeIfPresent(String.self, forKey: .regionKladrId)
- regionType = try values.decodeIfPresent(String.self, forKey: .regionType)
- regionTypeFull = try values.decodeIfPresent(String.self, forKey: .regionTypeFull)
- regionWithType = try values.decodeIfPresent(String.self, forKey: .regionWithType)
- settlement = try values.decodeIfPresent(String.self, forKey: .settlement)
- settlementFiasId = try values.decodeIfPresent(String.self, forKey: .settlementFiasId)
- settlementKladrId = try values.decodeIfPresent(String.self, forKey: .settlementKladrId)
- settlementType = try values.decodeIfPresent(String.self, forKey: .settlementType)
- settlementTypeFull = try values.decodeIfPresent(String.self, forKey: .settlementTypeFull)
- settlementWithType = try values.decodeIfPresent(String.self, forKey: .settlementWithType)
- source = try values.decodeIfPresent(String.self, forKey: .source)
- squareMeterPrice = try values.decodeIfPresent(String.self, forKey: .squareMeterPrice)
- street = try values.decodeIfPresent(String.self, forKey: .street)
- streetFiasId = try values.decodeIfPresent(String.self, forKey: .streetFiasId)
- streetKladrId = try values.decodeIfPresent(String.self, forKey: .streetKladrId)
- streetType = try values.decodeIfPresent(String.self, forKey: .streetType)
- streetTypeFull = try values.decodeIfPresent(String.self, forKey: .streetTypeFull)
- streetWithType = try values.decodeIfPresent(String.self, forKey: .streetWithType)
- taxOffice = try values.decodeIfPresent(String.self, forKey: .taxOffice)
- taxOfficeLegal = try values.decodeIfPresent(String.self, forKey: .taxOfficeLegal)
- timezone = try values.decodeIfPresent(String.self, forKey: .timezone)
- unparsedParts = try values.decodeIfPresent(String.self, forKey: .unparsedParts)
- }
-}
-
-/// Structure holding metro station name, name of a line and distance to suggested address.
-/// If there aren't metro stations nearby or API token used not subscribed to "Maximal" package
-/// `nil` is returned instead.
-public struct Metro: Decodable{
- public let name : String?
- public let line : String?
- public let distance : String?
-
- enum CodingKeys: String, CodingKey {
- case name, line, distance
- }
-
- public init(from decoder: Decoder) throws {
- let values = try decoder.container(keyedBy: CodingKeys.self)
- name = try values.decodeIfPresent(String.self, forKey: .name)
- line = try values.decodeIfPresent(String.self, forKey: .line)
-
- distance = values.decodeJSONNumber(forKey: CodingKeys.distance)
- }
-}
diff --git a/IIDadata/Sources/Model/ReverseGeocodeQuery.swift b/IIDadata/Sources/Model/ReverseGeocodeQuery.swift
deleted file mode 100644
index 636e6d1..0000000
--- a/IIDadata/Sources/Model/ReverseGeocodeQuery.swift
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// ReverseGeocodeQuery.swift
-// IIDadata
-//
-// Created by Yachin Ilya on 12.05.2020.
-//
-
-import Foundation
-
-///ReverseGeocodeQuery represents an serializable object used to perform reverse geocode queries.
-public class ReverseGeocodeQuery: Encodable, DadataQueryProtocol{
- let latitude: Double
- let longitude: Double
- let endpoint: String
- public var resultsCount: Int? = 10
- public var language: QueryResultLanguage?
- public var searchRadius: Int?
-
- ///New instance of ReverseGeocodeQuery.
- ///- Parameter query: Query should contain latitude and longitude of the point of interest.
- ///- Parameter delimeter: Single character delimeter to separate latitude and longitude.
- ///- Throws: May throw if parsing of latitude and longitude out of query fails.
- public convenience init(query: String, delimeter: Character = ",") throws {
- let splitStr = query.split(separator: delimeter)
-
- let latStr = String(splitStr[0]).trimmingCharacters(in: .whitespacesAndNewlines)
- let lonStr = String(splitStr[1]).trimmingCharacters(in: .whitespacesAndNewlines)
-
- guard let latitude = Double(latStr),
- let longitude = Double(lonStr)
- else {
- throw NSError(domain: "Dadata ReverseGeocodeQuery",
- code: -1,
- userInfo: ["description" : "Failed to parse coordinates from \(query) uding delimeter \(delimeter)"]
- )
- }
-
- self.init(latitude: latitude, longitude: longitude)
- }
-
- ///New instance of ReverseGeocodeQuery.
- ///- Parameter latitude: Latitude of the point of interest.
- ///- Parameter longitude: Longitude of the point of interest.
- public required init(latitude: Double, longitude: Double){
- self.latitude = latitude
- self.longitude = longitude
- self.endpoint = Constants.revGeocodeEndpoint
- }
-
- ///Serializes ReverseGeocodeQuery to send over the wire.
- func toJSON() throws -> Data {
- return try JSONEncoder().encode(self)
- }
-
- ///Returns an API endpoint for reverse geocode query.
- func queryEndpoint() -> String { return endpoint }
-
- enum CodingKeys: String, CodingKey {
- case latitude = "lat"
- case longitude = "lon"
- case resultsCount = "count"
- case language
- case searchRadius = "radius_meters"
- }
-}
diff --git a/IIDadata/Tests/IIDadataTests/IIDadataTests.swift b/IIDadata/Tests/IIDadataTests/IIDadataTests.swift
index 1f3c6c4..7df2951 100644
--- a/IIDadata/Tests/IIDadataTests/IIDadataTests.swift
+++ b/IIDadata/Tests/IIDadataTests/IIDadataTests.swift
@@ -1,15 +1,108 @@
-import XCTest
@testable import IIDadata
+import XCTest
final class IIDadataTests: XCTestCase {
- func testExample() {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct
- // results.
-// XCTAssertEqual(IIDadata().text, "Hello, World!")
+ // Properties
+
+// let apiKey = "abadf779d0525bebb9e16b72a97eabf4f7143292"
+
+ // Computed Properties
+
+ // Environment Variable is read from Scheme Configuration
+ private var apiToken: String {
+ return ProcessInfo.processInfo.environment["IIDadataAPIToken"] ?? ""
+ }
+
+ // Functions
+
+ // Address Suggestion Tests
+ func testSuggestAddress_BasicQuery() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestAddress("Москва, Красная площадь")
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ }
+
+ func testSuggestAddress_FilterByRegion() async throws {
+ var constraint = AddressQueryConstraint()
+ constraint.region = "Москва"
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestAddress(
+ "Москва, Красная площадь",
+ constraints: [constraint]
+ )
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ XCTAssertEqual(suggestions.suggestions?.first?.data?.region, "Москва")
+ }
+
+ func testSuggestAddress_FilterByCity() async throws {
+ var constraint = AddressQueryConstraint()
+ constraint.city = "Москва"
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestAddress(
+ "Москва, Красная площадь",
+ constraints: [constraint]
+ )
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ XCTAssertEqual(suggestions.suggestions?.first?.data?.city, "Москва")
+ }
+
+ func testSuggestAddress_FindByID() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestByKLADRFIAS("9120b43f-2fae-4838-a144-85e43c2bfb29")
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ }
+
+ func testReverseGeocode_LatLon() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .reverseGeocode(latitude: 55.755826, longitude: 37.617300)
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ }
+
+ func testReverseGeocode_LatLonString() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .reverseGeocode(query: "55.755826, 37.617300")
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ }
+
+ func testReverseGeocode_WithRadius() async throws {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .reverseGeocode(latitude: 55.755826, longitude: 37.617300, searchRadius: 100)
+ XCTAssertGreaterThan(suggestions.suggestions?.count ?? 0, 0)
+ }
+
+ // FIO Suggestion Tests
+ func testSuggestFIO_BasicQuery() async throws {
+ do {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken).suggestFio("Иванов")
+ dump(suggestions, name: "FIO_BasicQuery ")
+ XCTAssertGreaterThan(suggestions.count, 0)
+
+ } catch {
+ XCTFail(String(describing: error))
}
+ }
- static var allTests = [
- ("testExample", testExample),
- ]
+ func testSuggestFIO_FilterByGender() async throws {
+ do {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestFio("Иванов", gender: .male)
+ dump(suggestions, name: "FIO_FilterByGender ")
+ XCTAssertGreaterThan(suggestions.count, 0)
+
+ } catch {
+ XCTFail(String(describing: error))
+ }
+ }
+
+ func testSuggestFIO_FilterByParts() async throws {
+ do {
+ let suggestions = try await DadataSuggestions(apiKey: apiToken)
+ .suggestFio("Иванов Иван", parts: [.surname, .name])
+ dump(suggestions, name: "FIO_FilterByParts suggestions")
+ XCTAssertGreaterThan(suggestions.count, 0)
+
+ } catch {
+ XCTFail(String(describing: error))
+ }
+ }
}
diff --git a/Package.swift b/Package.swift
index 9def5ab..25fdc2e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,30 +1,56 @@
-// swift-tools-version:5.1
+// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
- name: "IIDadata",
- products: [
- // Products define the executables and libraries produced by a package, and make them visible to other packages.
- .library(
- name: "IIDadata",
- targets: ["IIDadata"]),
- ],
- dependencies: [
- // Dependencies declare other packages that this package depends on.
- // .package(url: /* package url */, from: "1.0.0"),
- ],
- targets: [
- // Targets are the basic building blocks of a package. A target can define a module or a test suite.
- // Targets can depend on other targets in this package, and on products in packages which this package depends on.
- .target(
- name: "IIDadata",
- dependencies: [],
- path: "IIDadata/Sources/"),
- .testTarget(
- name: "IIDadataTests",
- dependencies: ["IIDadata"],
- path: "IIDadata/Tests/IIDadataTests/"),
- ]
+ name: "IIDadata",
+ defaultLocalization: "ru",
+ platforms: [
+ .iOS(.v14),
+ .macOS(.v11),
+ .tvOS(.v14),
+ .watchOS(.v7),
+ ],
+ products: [
+ // Products define the executables and libraries produced by a package, and make them visible to other packages.
+ .library(
+ name: "IIDadata",
+ targets: ["IIDadata"]
+ ),
+ .library(
+ name: "IIDadataUI",
+ targets: ["IIDadata", "IIDadataUI"]
+ ),
+ ],
+ dependencies: [
+ // Dependencies declare other packages that this package depends on.
+ // .package(url: /* package url */, from: "1.0.0"),
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+ // Targets can depend on other targets in this package, and on products in packages which this package depends on.
+ .executableTarget(
+ name: "Example",
+ dependencies: ["IIDadata", "IIDadataUI"],
+ path: "IIDadata/Example/"
+ ),
+ .target(
+ name: "IIDadata",
+ dependencies: [],
+ swiftSettings: [
+ .define("SWIFT_PACKAGE"),
+ .unsafeFlags(["-enable-library-evolution"]),
+ ]
+ ),
+ .target(
+ name: "IIDadataUI",
+ dependencies: ["IIDadata"]
+ ),
+ .testTarget(
+ name: "IIDadataTests",
+ dependencies: ["IIDadata"],
+ path: "IIDadata/Tests/IIDadataTests/"
+ ),
+ ]
)
diff --git a/IIDadata/Sources/.gitkeep b/Sources/IIDadata/.gitkeep
similarity index 100%
rename from IIDadata/Sources/.gitkeep
rename to Sources/IIDadata/.gitkeep
diff --git a/Sources/IIDadata/Constants.swift b/Sources/IIDadata/Constants.swift
new file mode 100644
index 0000000..42eace3
--- /dev/null
+++ b/Sources/IIDadata/Constants.swift
@@ -0,0 +1,37 @@
+//
+// Constants.swift
+// IIDadata
+//
+// Created by Yachin Ilya on 11.05.2020.
+//
+
+import Foundation
+
+// MARK: - Constants
+
+enum Constants {
+ static let suggestionsAPIURL = "http://suggestions.dadata.ru/suggestions/api/4_1/rs/"
+ static let fioSuggestionsAPIURL = "http://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/fio"
+ static let addressEndpoint = AddressQueryType.address.rawValue
+ static let addressFIASOnlyEndpoint = AddressQueryType.fiasOnly.rawValue
+ static let addressByIDEndpoint = AddressQueryType.findByID.rawValue
+ static let revGeocodeEndpoint = "geolocate/address"
+ static let infoPlistTokenKey = "IIDadataAPIToken"
+}
+
+// MARK: - AddressQueryType
+
+/// API endpoints for different request types.
+public enum AddressQueryType: String {
+ case address = "suggest/address"
+ case fiasOnly = "suggest/fias"
+ case findByID = "findById/address"
+}
+
+// MARK: - QueryResultLanguage
+
+/// Language of response.
+public enum QueryResultLanguage: String, Encodable {
+ case ru
+ case en
+}
diff --git a/Sources/IIDadata/DadataQueryProtocol.swift b/Sources/IIDadata/DadataQueryProtocol.swift
new file mode 100644
index 0000000..0a9fd22
--- /dev/null
+++ b/Sources/IIDadata/DadataQueryProtocol.swift
@@ -0,0 +1,27 @@
+//
+// DadataQueryProtocol.swift
+// IIDadata
+//
+// Created by Yachin Ilya on 12.05.2020.
+//
+
+import Foundation
+
+/// Conformance to this protocol requires implementing methods to return a query endpoint string and to convert the entity to JSON.
+/// Protocol that defines the blueprint for generating a query endpoint and converting data to JSON.
+protocol DadataQueryProtocol {
+ /// Generates and returns the endpoint string for the query.
+ ///
+ /// The implementation should provide the specific endpoint required for the Dadata API request.
+ ///
+ /// - Returns: A `String` representing the query endpoint.
+ func queryEndpoint() -> String
+
+ /// Converts the implementing entity to JSON format.
+ /// If the object cannot be serialized, this method throws an error.
+ /// This method should provide a way to serialize the entity's data into a JSON `Data` object.
+ ///
+ /// - Returns: A `Data` object containing the JSON representation of the entity.
+ /// - Throws: An error if the serialization fails.
+ func toJSON() throws -> Data
+}
diff --git a/Sources/IIDadata/DadataSuggestions.swift b/Sources/IIDadata/DadataSuggestions.swift
new file mode 100644
index 0000000..9f9db63
--- /dev/null
+++ b/Sources/IIDadata/DadataSuggestions.swift
@@ -0,0 +1,378 @@
+import protocol Combine.ObservableObject
+import Foundation
+
+@available(iOS 14.0, *)
+public actor DadataSuggestions: ObservableObject {
+ // Static Properties
+
+ private static var sharedInstance: DadataSuggestions?
+
+ // Properties
+
+ /// API key for [Dadata](https://dadata.ru/profile/#info).
+ private let apiKey: String
+
+ /// Base URL of suggestions API
+ private var suggestionsAPIURL: URL
+
+ // Lifecycle
+
+ /// New instance of DadataSuggestions.
+ ///
+ ///
+ /// Required API key is read from Info.plist. Each init creates new instance using same token.
+ /// If DadataSuggestions is used havily consider `DadataSuggestions.shared()` instead.
+ /// - Precondition: Token set with "IIDadataAPIToken" key in Info.plist.
+ /// - Throws: Call may throw if there isn't a value for key "IIDadataAPIToken" set in Info.plist.
+ public init() throws {
+ let key = try DadataSuggestions.readAPIKeyFromPlist()
+ self.init(apiKey: key)
+ Self.sharedInstance = self
+ }
+
+ /// This init checks connectivity once the class instance is set.
+ ///
+ /// This init should not be called on main thread as it may take up long time as it makes request to server in a blocking manner.
+ /// Throws if connection is impossible or request is timed out.
+ /// ```
+ /// DispatchQueue.global(qos: .background).async {
+ /// let dadata = try DadataSuggestions(apiKey: " ", checkWithTimeout: 15)
+ /// }
+ /// ```
+ /// - Parameter apiKey: Dadata API token. Check it in account settings at dadata.ru.
+ /// - Parameter checkWithTimeout: Time in seconds to wait for response.
+ ///
+ /// - Throws: May throw on connectivity problems, missing or wrong API token, limits exeeded, wrong endpoint.
+ /// May throw if request is timed out.
+ public init(api: String /* , checkWithTimeout timeout: Int */ ) throws {
+ self.init(apiKey: api)
+ Task { try await checkAPIConnectivity() }
+ Self.sharedInstance = self
+ }
+
+ /// New instance of DadataSuggestions.
+ /// - Parameter apiKey: Dadata API token. Check it in account settings at dadata.ru.
+ public /* required */ init(apiKey: String) {
+ self.init(apiKey: apiKey, url: Constants.suggestionsAPIURL)
+ Self.sharedInstance = self
+ }
+
+ private init(apiKey: String, url: String) {
+ self.apiKey = apiKey
+ suggestionsAPIURL = URL(string: url)!
+ }
+
+ // Static Functions
+
+ /// Get shared instance of DadataSuggestions class.
+ ///
+ /// Call may throw if neither apiKey parameter is provided
+ /// nor a value for key "IIDadataAPIToken" is set in Info.plist
+ /// whenever shared instance weren't instantiated earlier.
+ /// If another apiKey provided new shared instance of DadataSuggestions recreated with the provided API token.
+ /// - Parameter apiKey: Dadata API token. Check it in account settings at dadata.ru.
+ public static func shared(apiKey: String? = nil) throws -> DadataSuggestions {
+ if let instance = sharedInstance, instance.apiKey == apiKey || apiKey == nil { return instance }
+
+ if let key = apiKey {
+ sharedInstance = DadataSuggestions(apiKey: key)
+ return sharedInstance!
+ }
+
+ let key = try readAPIKeyFromPlist()
+ sharedInstance = DadataSuggestions(apiKey: key)
+ return sharedInstance!
+ }
+
+ private static func readAPIKeyFromPlist() throws -> String {
+ var dictionary: NSDictionary?
+ if let path = Bundle.main.path(forResource: "Info", ofType: "plist") {
+ dictionary = NSDictionary(contentsOfFile: path)
+ }
+ guard let key = dictionary?.value(forKey: Constants.infoPlistTokenKey) as? String else {
+ throw NSError(domain: "Dadata API key missing in Info.plist", code: 1, userInfo: nil)
+ }
+ return key
+ }
+
+ // Functions
+
+ /// Basic address suggestions request with only rquired data.
+ ///
+ /// - Parameter query: Query string to send to API. String of a free-form e.g. address part.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ public func suggestAddress(_ query: String) async throws -> AddressSuggestionResponse {
+ try await suggestAddress(AddressSuggestionQuery(query))
+ }
+
+ /// Address suggestions request.
+ ///
+ /// Limitations, filters and constraints may be applied to query.
+ ///
+ /// - Parameter query: Query string to send to API. String of a free-form e.g. address part.
+ /// - Parameter queryType: Lets select whether the request type. There are 3 query types available:
+ /// `address` — standart address suggestion query;
+ /// `fiasOnly` — query to only search in FIAS database: less matches, state provided address data only;
+ /// `findByID` — takes KLADR or FIAS ID as a query parameter to lookup additional data.
+ /// - Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
+ /// including latitude and longitude. `20` is a maximum value.
+ /// - Parameter language: Suggested results may be in Russian or English.
+ /// - Parameter constraints: List of `AddressQueryConstraint` objects to filter results.
+ /// - Parameter regionPriority: List of RegionPriority objects to prefer in lookup.
+ /// - Parameter upperScaleLimit: Bigger `ScaleLevel` object in pair of scale limits.
+ /// - Parameter lowerScaleLimit: Smaller `ScaleLevel` object in pair of scale limits.
+ /// - Parameter trimRegionResult: Remove region and city names from suggestion top level.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ @Sendable public func suggestAddress(
+ _ query: String,
+ queryType: AddressQueryType = .address,
+ resultsCount: Int? = 10,
+ language: QueryResultLanguage? = nil,
+ constraints: [AddressQueryConstraint]? = nil,
+ regionPriority: [RegionPriority]? = nil,
+ upperScaleLimit: ScaleLevel? = nil,
+ lowerScaleLimit: ScaleLevel? = nil,
+ trimRegionResult: Bool = false
+ ) async throws -> AddressSuggestionResponse {
+ let suggestionQuery = AddressSuggestionQuery(query, ofType: queryType)
+
+ suggestionQuery.resultsCount = resultsCount
+ suggestionQuery.language = language
+ suggestionQuery.constraints = constraints
+ suggestionQuery.regionPriority = regionPriority
+ suggestionQuery.upperScaleLimit = upperScaleLimit != nil ? ScaleBound(value: upperScaleLimit) : nil
+ suggestionQuery.lowerScaleLimit = lowerScaleLimit != nil ? ScaleBound(value: lowerScaleLimit) : nil
+ suggestionQuery.trimRegionResult = trimRegionResult
+
+ return try await suggestAddress(suggestionQuery)
+ }
+
+ /// Address suggestions request.
+ ///
+ /// Allows to pass most of arguments as a strings converting to internally used classes.
+ ///
+ /// - Parameter query: Query string to send to API. String of a free-form e.g. address part.
+ /// - Parameter queryType: Lets select whether the request type. There are 3 query types available:
+ /// `address` — standart address suggestion query;
+ /// `fiasOnly` — query to only search in FIAS database: less matches, state provided address data only;
+ /// `findByID` — takes KLADR or FIAS ID as a query parameter to lookup additional data.
+ /// - Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
+ /// including latitude and longitude. `20` is a maximum value.
+ /// - Parameter language: Suggested results in "ru" — Russian or "en" — English.
+ /// - Parameter constraints: Literal JSON string formated according to
+ /// [Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=204669108).
+ /// - Parameter regionPriority: List of regions' KLADR IDs to prefer in lookup as shown in
+ /// [Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=285343795).
+ /// - Parameter upperScaleLimit: Bigger sized object in pair of scale limits.
+ /// - Parameter lowerScaleLimit: Smaller sized object in pair of scale limits. Both can take following values:
+ /// `country` — Страна,
+ /// `region` — Регион,
+ /// `area` — Район,
+ /// `city` — Город,
+ /// `settlement` — Населенный пункт,
+ /// `street` — Улица,
+ /// `house` — Дом,
+ /// `country` — Страна,
+ /// - Parameter trimRegionResult: Remove region and city names from suggestion top level.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ /// - Throws: ``DadataError`` if something went wrong.
+ public func suggestAddress(
+ _ query: String,
+ queryType: AddressQueryType = .address,
+ resultsCount: Int? = 10,
+ language: String? = nil,
+ constraints: [String]? = nil,
+ regionPriority: [String]? = nil,
+ upperScaleLimit: String? = nil,
+ lowerScaleLimit: String? = nil,
+ trimRegionResult: Bool = false
+ ) async throws -> AddressSuggestionResponse {
+ let queryConstraints: [AddressQueryConstraint]? = try constraints?.compactMap {
+ if let data = $0.data(using: .utf8) {
+ return try JSONDecoder().decode(AddressQueryConstraint.self, from: data)
+ }
+ return nil
+ }
+ let preferredRegions: [RegionPriority]? = regionPriority?.compactMap { RegionPriority(kladr_id: $0) }
+
+ return try await suggestAddress(
+ query,
+ queryType: queryType,
+ resultsCount: resultsCount,
+ language: QueryResultLanguage(rawValue: language ?? "ru"),
+ constraints: queryConstraints,
+ regionPriority: preferredRegions,
+ upperScaleLimit: ScaleLevel(rawValue: upperScaleLimit ?? "*"),
+ lowerScaleLimit: ScaleLevel(rawValue: lowerScaleLimit ?? "*"),
+ trimRegionResult: trimRegionResult
+ )
+ }
+
+ /// Basic address suggestions request to only search in FIAS database: less matches, state provided address data only.
+ ///
+ /// - Parameter query: Query string to send to API. String of a free-form e.g. address part.
+ /// - Returns:``AddressSuggestionResponse`` - result: result of address suggestion query.
+ public func suggestAddressFromFIAS(_ query: String) async throws -> AddressSuggestionResponse {
+ try await suggestAddress(AddressSuggestionQuery(query, ofType: .fiasOnly))
+ }
+
+ /// Basic address suggestions request takes KLADR or FIAS ID as a query parameter to lookup additional data.
+ ///
+ /// - Parameter query: KLADR or FIAS ID.
+ /// - Returns:``AddressSuggestionResponse`` - result: result of address suggestion query.
+ public func suggestByKLADRFIAS(_ query: String) async throws -> AddressSuggestionResponse {
+ try await suggestAddress(AddressSuggestionQuery(query, ofType: .findByID))
+ }
+
+ /// Address suggestion request with custom `AddressSuggestionQuery`.
+ ///
+ /// - Parameter query: Query object.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ public func suggestAddress(_ query: AddressSuggestionQuery) async throws -> AddressSuggestionResponse {
+ try await fetchResponse(withQuery: query)
+ }
+
+ /// Reverse Geocode request with latitude and longitude as a single string.
+ ///
+ /// - Throws: May throw if query is malformed.
+ ///
+ /// - Parameter query: Latitude and longitude as a string. Should have single character separator.
+ /// - Parameter delimeter: Character to separate latitude and longitude. Defaults to '`,`'
+ /// - Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
+ /// including latitude and longitude. `20` is a maximum value.
+ /// - Parameter language: Suggested results in "ru" — Russian or "en" — English.
+ /// - Parameter searchRadius: Radius to suggest objects nearest to coordinates point.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ public func reverseGeocode(
+ query: String,
+ delimiter: Character = ",",
+ resultsCount: Int? = 10,
+ language: String? = "ru",
+ searchRadius: Int? = nil
+ ) async throws -> AddressSuggestionResponse {
+ let geoquery = try ReverseGeocodeQuery(query: query, delimeter: delimiter)
+ geoquery.resultsCount = resultsCount
+ geoquery.language = QueryResultLanguage(rawValue: language ?? "ru")
+ geoquery.searchRadius = searchRadius
+
+ return try await reverseGeocode(geoquery)
+ }
+
+ /// Reverse Geocode request with latitude and longitude as a single string.
+ ///
+ /// - Parameter latitude: Latitude.
+ /// - Parameter longitude: Longitude.
+ /// - Parameter resultsCount: How many suggestions to return. `1` provides more data on a single object
+ /// including latitude and longitude. `20` is a maximum value.
+ /// - Parameter language: Suggested results may be in Russian or English.
+ /// - Parameter searchRadius: Radius to suggest objects nearest to coordinates point.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ public func reverseGeocode(
+ latitude: Double,
+ longitude: Double,
+ resultsCount: Int? = 10,
+ language: QueryResultLanguage? = nil,
+ searchRadius: Int? = nil
+ ) async throws -> AddressSuggestionResponse {
+ let geoquery = ReverseGeocodeQuery(latitude: latitude, longitude: longitude)
+ geoquery.resultsCount = resultsCount
+ geoquery.language = language
+ geoquery.searchRadius = searchRadius
+
+ return try await fetchResponse(withQuery: geoquery)
+ }
+
+ /// Reverse geocode request with custom `ReverseGeocodeQuery`.
+ ///
+ /// - Parameter query: Query object.
+ /// - Returns:``AddressSuggestionResponse`` - result of address suggestion query.
+ public func reverseGeocode(_ query: ReverseGeocodeQuery) async throws -> AddressSuggestionResponse {
+ try await fetchResponse(withQuery: query)
+ }
+
+ /// Suggests a list of FIO (Family, Given, and Middle Names) based on the provided query.
+ ///
+ /// This asynchronous method fetches suggestions for FIO (Family, Given, and Middle Names) from a remote server.
+ ///
+ ///
+ /// - Parameter query: A string containing the name or partial name for which suggestions are required.
+ /// - Parameter count: The maximum number of suggestions to return. Defaults to 10 if not specified.
+ /// - Parameter gender: The `gender` of the person. Defaults to `nil` if not specified.
+ /// - Parameter parts: Indicates if the `fullname parts` should be returned separately. Defaults to `nil` if not specified.
+ /// - Returns: An array of `FioSuggestion` objects matching the query.
+ /// - Throws: An error if the request fails or the server returns an error.
+ ///
+ /// This method constructs a `FioSuggestionQuery` object with the given query and count, then fetches the response
+ /// using the `fetchResponse(withQuery:)` method.
+ public func suggestFio(
+ _ query: String,
+ count: Int = 10,
+ gender: Gender? = nil,
+ parts: [FioSuggestionQuery.Part]? = nil
+ ) async throws -> [FioSuggestion] {
+ let fioSuggestionQuery = FioSuggestionQuery(
+ query,
+ count: count,
+ parts: parts,
+ gender: gender
+ )
+ debugPrint("FioSuggestionQuery: \n \(fioSuggestionQuery)")
+ let fioSuggestionResponse: FioSuggestionResponse = try await fetchResponse(withQuery: fioSuggestionQuery)
+ debugPrint(fioSuggestionResponse)
+ return fioSuggestionResponse.suggestions
+ }
+
+ func checkAPIConnectivity() async throws {
+ let request = createRequest(url: suggestionsAPIURL.appendingPathComponent(Constants.addressEndpoint))
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse,
+ (200 ... 299).contains(httpResponse.statusCode)
+ else {
+ dump(response, name: "API Connectivity Response")
+ throw nonOKResponseToError(response: (response as? HTTPURLResponse) ?? .init(), body: data)
+ }
+ }
+
+ // MARK: - Private
+
+ private func createRequest(url: URL) -> URLRequest {
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.addValue("application/json", forHTTPHeaderField: "Accept")
+ request.addValue("Token " + apiKey, forHTTPHeaderField: "Authorization")
+
+ dump(request, name: "Request \(String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "Unable to decode request body")")
+ return request
+ }
+
+ private func nonOKResponseToError(response: HTTPURLResponse, body data: Data?) -> Error {
+ let code = response.statusCode
+ var info: [String: Any] = [:]
+ response.allHeaderFields.forEach { if let k = $0.key as? String { info[k] = $0.value } }
+ if let data = data {
+ let object = try? JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any]
+ object?.forEach { if let k = $0.key as? String { info[k] = $0.value } }
+ }
+ return NSError(domain: "HTTP Status \(HTTPURLResponse.localizedString(forStatusCode: code))", code: code, userInfo: info)
+ }
+
+ private func fetchResponse(withQuery query: DadataQueryProtocol) async throws -> T {
+ var request = createRequest(url: suggestionsAPIURL.appendingPathComponent(query.queryEndpoint()))
+ request.httpBody = try query.toJSON()
+ dump(String(data: request.httpBody ?? Data(), encoding: .utf8), name: "Request \(T.self)")
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw NSError(domain: "Invalid response", code: 0, userInfo: nil)
+ }
+
+ guard (200 ... 299).contains(httpResponse.statusCode) else {
+ throw NSError(domain: "HTTP Error", code: httpResponse.statusCode, userInfo: ["description": httpResponse.description])
+ }
+
+ return try JSONDecoder().decode(T.self, from: data)
+ }
+}
diff --git a/Sources/IIDadata/Extension/KeyedDecodingContainer+decodeJSONNumber.swift b/Sources/IIDadata/Extension/KeyedDecodingContainer+decodeJSONNumber.swift
new file mode 100644
index 0000000..558426e
--- /dev/null
+++ b/Sources/IIDadata/Extension/KeyedDecodingContainer+decodeJSONNumber.swift
@@ -0,0 +1,26 @@
+//
+// KeyedDecodingContainer+decodeJSONNumber.swift
+// IIDadata
+//
+// Created by Yachin Ilya on 23.07.2020.
+//
+
+import Foundation
+
+extension KeyedDecodingContainer {
+ /// Forces integers and floating point numbers to optional String type.
+ /// Helpful when JSON response may include inconsistency in number fields.
+ /// - parameter key: CodingKey of Decodable object CodingKeys to lookup.
+ func decodeJSONNumber(forKey key: CodingKey) -> String? {
+ if let v = try? decode(String.self, forKey: key as! K) {
+ return v
+ }
+ if let v = try? decode(Int.self, forKey: key as! K) {
+ return "\(v)"
+ }
+ if let v = try? decode(Double.self, forKey: key as! K) {
+ return "\(v)"
+ }
+ return nil
+ }
+}
diff --git a/Sources/IIDadata/Model/Address/AddressSuggestion.swift b/Sources/IIDadata/Model/Address/AddressSuggestion.swift
new file mode 100644
index 0000000..ddaf1cd
--- /dev/null
+++ b/Sources/IIDadata/Model/Address/AddressSuggestion.swift
@@ -0,0 +1,550 @@
+//
+// AddressSuggestionData.swift
+// IIDadata
+//
+// Created by NSFuntik on 09.08.2024.
+//
+
+// MARK: - AddressSuggestion
+
+/// Every single suggestion is represented as AddressSuggestions.
+public struct AddressSuggestion: Suggestion {
+ public static func == (lhs: AddressSuggestion, rhs: AddressSuggestion) -> Bool {
+ lhs.value == rhs.value && lhs.unrestrictedValue == rhs.unrestrictedValue
+ }
+
+ /// Address in short format.
+ public let value: String
+ /// All the data returned in response to suggestion query.
+ public let data: AddressData?
+ /// Address in long format with region.
+ public let unrestrictedValue: String?
+
+ public init(
+ value: String,
+ data: AddressData?,
+ unrestrictedValue: String?
+ ) {
+ self.value = value
+ self.data = data
+ self.unrestrictedValue = unrestrictedValue
+ }
+}
+
+// MARK: AddressSuggestion.AddressData
+
+public extension AddressSuggestion {
+ /// All the data returned in response to suggestion query.
+
+ enum CodingKeys: String, CodingKey {
+ case value
+ case data
+ case unrestrictedValue = "unrestricted_value"
+ }
+
+ struct AddressData: Decodable {
+ // Nested Types
+
+ enum CodingKeys: String, CodingKey {
+ case area
+ case areaFiasId = "area_fias_id"
+ case areaKladrId = "area_kladr_id"
+ case areaType = "area_type"
+ case areaTypeFull = "area_type_full"
+ case areaWithType = "area_with_type"
+ case beltwayDistance = "beltway_distance"
+ case beltwayHit = "beltway_hit"
+ case block
+ case blockType = "block_type"
+ case blockTypeFull = "block_type_full"
+ case building
+ case buildingType = "building_type"
+ case cadastralNumber = "cadastral_number"
+ case capitalMarker = "capital_marker"
+ case city
+ case cityArea = "city_area"
+ case cityDistrict = "city_district"
+ case cityDistrictFiasId = "city_district_fias_id"
+ case cityDistrictKladrId = "city_district_kladr_id"
+ case cityDistrictType = "city_district_type"
+ case cityDistrictTypeFull = "city_district_type_full"
+ case cityDistrictWithType = "city_district_with_type"
+ case cityFiasId = "city_fias_id"
+ case cityKladrId = "city_kladr_id"
+ case cityType = "city_type"
+ case cityTypeFull = "city_type_full"
+ case cityWithType = "city_with_type"
+ case country
+ case countryIsoCode = "country_iso_code"
+ case federalDistrict = "federal_district"
+ case fiasActualityState = "fias_actuality_state"
+ case fiasCode = "fias_code"
+ case fiasId = "fias_id"
+ case fiasLevel = "fias_level"
+ case flat
+ case flatFiasId = "flat_fias_id"
+ case flatArea = "flat_area"
+ case flatPrice = "flat_price"
+ case flatType = "flat_type"
+ case flatTypeFull = "flat_type_full"
+ case geoLat = "geo_lat"
+ case geoLon = "geo_lon"
+ case geonameId = "geoname_id"
+ case historyValues = "history_values"
+ case house
+ case houseFiasId = "house_fias_id"
+ case houseKladrId = "house_kladr_id"
+ case houseType = "house_type"
+ case houseTypeFull = "house_type_full"
+ case kladrId = "kladr_id"
+ case metro
+ case okato
+ case oktmo
+ case planningStructure = "planning_structure"
+ case planningStructureFiasId = "planning_structure_fias_id"
+ case planningStructureKladrId = "planning_structure_kladr_id"
+ case planningStructureType = "planning_structure_type"
+ case planningStructureTypeFull = "planning_structure_type_full"
+ case planningStructureWithType = "planning_structure_with_type"
+ case postalBox = "postal_box"
+ case postalCode = "postal_code"
+ case qc
+ case qcComplete = "qc_complete"
+ case qcGeo = "qc_geo"
+ case qcHouse = "qc_house"
+ case region
+ case regionFiasId = "region_fias_id"
+ case regionIsoCode = "region_iso_code"
+ case regionKladrId = "region_kladr_id"
+ case regionType = "region_type"
+ case regionTypeFull = "region_type_full"
+ case regionWithType = "region_with_type"
+ case settlement
+ case settlementFiasId = "settlement_fias_id"
+ case settlementKladrId = "settlement_kladr_id"
+ case settlementType = "settlement_type"
+ case settlementTypeFull = "settlement_type_full"
+ case settlementWithType = "settlement_with_type"
+ case source
+ case squareMeterPrice = "square_meter_price"
+ case street
+ case streetFiasId = "street_fias_id"
+ case streetKladrId = "street_kladr_id"
+ case streetType = "street_type"
+ case streetTypeFull = "street_type_full"
+ case streetWithType = "street_with_type"
+ case taxOffice = "tax_office"
+ case taxOfficeLegal = "tax_office_legal"
+ case timezone
+ case unparsedParts = "unparsed_parts"
+ }
+
+ // Properties
+
+ public let area: String?
+ public let areaFiasId: String?
+ public let areaKladrId: String?
+ public let areaType: String?
+ public let areaTypeFull: String?
+ public let areaWithType: String?
+ public let beltwayDistance: String?
+ public let beltwayHit: String?
+ public let block: String?
+ public let blockType: String?
+ public let blockTypeFull: String?
+ public let building: String?
+ public let buildingType: String?
+ public let cadastralNumber: String?
+ /// - `1` — subregion (district) center
+ /// - `2` — region center
+ /// - `3` — `1` and `2` combined
+ /// - `4` — main subregion (district) in region
+ /// - `0` — no status
+ public let capitalMarker: String?
+ public let city: String?
+ public let cityArea: String?
+ public let cityDistrict: String?
+ public let cityDistrictFiasId: String?
+ public let cityDistrictKladrId: String?
+ public let cityDistrictType: String?
+ public let cityDistrictTypeFull: String?
+ public let cityDistrictWithType: String?
+ public let cityFiasId: String?
+ public let cityKladrId: String?
+ public let cityType: String?
+ public let cityTypeFull: String?
+ public let cityWithType: String?
+ public let country: String?
+ public let countryIsoCode: String?
+ public let federalDistrict: String?
+ /// FIAS actuality
+ /// - `0` — actual
+ /// - `1–50` — renamed
+ /// - `51` — changed
+ /// - `99` — removed
+ public let fiasActualityState: String?
+ /// Structure of FIAS code (СС+РРР+ГГГ+ППП+СССС+УУУУ+ДДДД)
+ public let fiasCode: String?
+ public let fiasId: String?
+ /// FIAS address precision
+ /// - `0` — country
+ /// - `1` — region
+ /// - `3` — subregion (district of region)
+ /// - `4` — city
+ /// - `5` — city district
+ /// - `6` — locality, neighbourhood, settlement etc.
+ /// - `7` — street
+ /// - `8` — building
+ /// - `9` — flat / apartment
+ /// - `65` — city plan unit
+ /// - `-1` — empty or abroad
+ public let fiasLevel: String?
+ public let flat: String?
+ public let flatFiasId: String?
+ public let flatArea: String?
+ public let flatPrice: String?
+ public let flatType: String?
+ public let flatTypeFull: String?
+ public let geoLat: String?
+ public let geoLon: String?
+ public let geonameId: String?
+ public let historyValues: [String]?
+ public let house: String?
+ public let houseFiasId: String?
+ public let houseKladrId: String?
+ public let houseType: String?
+ public let houseTypeFull: String?
+ public let kladrId: String?
+ public let metro: [Metro]?
+ public let okato: String?
+ public let oktmo: String?
+ public let planningStructure: String?
+ public let planningStructureFiasId: String?
+ public let planningStructureKladrId: String?
+ public let planningStructureType: String?
+ public let planningStructureTypeFull: String?
+ public let planningStructureWithType: String?
+ public let postalBox: String?
+ public let postalCode: String?
+ public let qc: String?
+ public let qcComplete: String?
+ /// Coordinates precision
+ /// - `0` — precise
+ /// - `1` — nearest building
+ /// - `2` — nearest street
+ /// - `3` — city district, locality, neighbourhood, settlement etc.
+ /// - `4` — city
+ /// - `5` — failed to determine coordinates
+ public let qcGeo: String?
+ public let qcHouse: String?
+ public let region: String?
+ public let regionFiasId: String?
+ public let regionIsoCode: String?
+ public let regionKladrId: String?
+ public let regionType: String?
+ public let regionTypeFull: String?
+ public let regionWithType: String?
+ public let settlement: String?
+ public let settlementFiasId: String?
+ public let settlementKladrId: String?
+ public let settlementType: String?
+ public let settlementTypeFull: String?
+ public let settlementWithType: String?
+ public let source: String?
+ public let squareMeterPrice: String?
+ public let street: String?
+ public let streetFiasId: String?
+ public let streetKladrId: String?
+ public let streetType: String?
+ public let streetTypeFull: String?
+ public let streetWithType: String?
+ public let taxOffice: String?
+ public let taxOfficeLegal: String?
+ public let timezone: String?
+ public let unparsedParts: String?
+
+ // Lifecycle
+ public init(
+ area: String? = nil,
+ areaFiasId: String? = nil,
+ areaKladrId: String? = nil,
+ areaType: String? = nil,
+ areaTypeFull: String? = nil,
+ areaWithType: String? = nil,
+ beltwayDistance: String? = nil,
+ beltwayHit: String? = nil,
+ block: String? = nil,
+ blockType: String? = nil,
+ blockTypeFull: String? = nil,
+ building: String? = nil,
+ buildingType: String? = nil,
+ cadastralNumber: String? = nil,
+ capitalMarker: String? = nil,
+ city: String? = nil,
+ cityArea: String? = nil,
+ cityDistrict: String? = nil,
+ cityDistrictFiasId: String? = nil,
+ cityDistrictKladrId: String? = nil,
+ cityDistrictType: String? = nil,
+ cityDistrictTypeFull: String? = nil,
+ cityDistrictWithType: String? = nil,
+ cityFiasId: String? = nil,
+ cityKladrId: String? = nil,
+ cityType: String? = nil,
+ cityTypeFull: String? = nil,
+ cityWithType: String? = nil,
+ country: String? = nil,
+ countryIsoCode: String? = nil,
+ federalDistrict: String? = nil,
+ fiasActualityState: String? = nil,
+ fiasCode: String? = nil,
+ fiasId: String? = nil,
+ fiasLevel: String? = nil,
+ flat: String? = nil,
+ flatFiasId: String? = nil,
+ flatArea: String? = nil,
+ flatPrice: String? = nil,
+ flatType: String? = nil,
+ flatTypeFull: String? = nil,
+ geoLat: String? = nil,
+ geoLon: String? = nil,
+ geonameId: String? = nil,
+ historyValues: [String]? = nil,
+ house: String? = nil,
+ houseFiasId: String? = nil,
+ houseKladrId: String? = nil,
+ houseType: String? = nil,
+ houseTypeFull: String? = nil,
+ kladrId: String? = nil,
+ metro: [Metro]? = nil,
+ okato: String? = nil,
+ oktmo: String? = nil,
+ planningStructure: String? = nil,
+ planningStructureFiasId: String? = nil,
+ planningStructureKladrId: String? = nil,
+ planningStructureType: String? = nil,
+ planningStructureTypeFull: String? = nil,
+ planningStructureWithType: String? = nil,
+ postalBox: String? = nil,
+ postalCode: String? = nil,
+ qc: String? = nil,
+ qcComplete: String? = nil,
+ qcGeo: String? = nil,
+ qcHouse: String? = nil,
+ region: String? = nil,
+ regionFiasId: String? = nil,
+ regionIsoCode: String? = nil,
+ regionKladrId: String? = nil,
+ regionType: String? = nil,
+ regionTypeFull: String? = nil,
+ regionWithType: String? = nil,
+ settlement: String? = nil,
+ settlementFiasId: String? = nil,
+ settlementKladrId: String? = nil,
+ settlementType: String? = nil,
+ settlementTypeFull: String? = nil,
+ settlementWithType: String? = nil,
+ source: String? = nil,
+ squareMeterPrice: String? = nil,
+ street: String? = nil,
+ streetFiasId: String? = nil,
+ streetKladrId: String? = nil,
+ streetType: String? = nil,
+ streetTypeFull: String? = nil,
+ streetWithType: String? = nil,
+ taxOffice: String? = nil,
+ taxOfficeLegal: String? = nil,
+ timezone: String? = nil,
+ unparsedParts: String? = nil
+ ) {
+ self.area = area
+ self.areaFiasId = areaFiasId
+ self.areaKladrId = areaKladrId
+ self.areaType = areaType
+ self.areaTypeFull = areaTypeFull
+ self.areaWithType = areaWithType
+ self.beltwayDistance = beltwayDistance
+ self.beltwayHit = beltwayHit
+ self.block = block
+ self.blockType = blockType
+ self.blockTypeFull = blockTypeFull
+ self.building = building
+ self.buildingType = buildingType
+ self.cadastralNumber = cadastralNumber
+ self.capitalMarker = capitalMarker
+ self.city = city
+ self.cityArea = cityArea
+ self.cityDistrict = cityDistrict
+ self.cityDistrictFiasId = cityDistrictFiasId
+ self.cityDistrictKladrId = cityDistrictKladrId
+ self.cityDistrictType = cityDistrictType
+ self.cityDistrictTypeFull = cityDistrictTypeFull
+ self.cityDistrictWithType = cityDistrictWithType
+ self.cityFiasId = cityFiasId
+ self.cityKladrId = cityKladrId
+ self.cityType = cityType
+ self.cityTypeFull = cityTypeFull
+ self.cityWithType = cityWithType
+ self.country = country
+ self.countryIsoCode = countryIsoCode
+ self.federalDistrict = federalDistrict
+ self.fiasActualityState = fiasActualityState
+ self.fiasCode = fiasCode
+ self.fiasId = fiasId
+ self.fiasLevel = fiasLevel
+ self.flat = flat
+ self.flatFiasId = flatFiasId
+ self.flatArea = flatArea
+ self.flatPrice = flatPrice
+ self.flatType = flatType
+ self.flatTypeFull = flatTypeFull
+ self.geoLat = geoLat
+ self.geoLon = geoLon
+ self.geonameId = geonameId
+ self.historyValues = historyValues
+ self.house = house
+ self.houseFiasId = houseFiasId
+ self.houseKladrId = houseKladrId
+ self.houseType = houseType
+ self.houseTypeFull = houseTypeFull
+ self.kladrId = kladrId
+ self.metro = metro
+ self.okato = okato
+ self.oktmo = oktmo
+ self.planningStructure = planningStructure
+ self.planningStructureFiasId = planningStructureFiasId
+ self.planningStructureKladrId = planningStructureKladrId
+ self.planningStructureType = planningStructureType
+ self.planningStructureTypeFull = planningStructureTypeFull
+ self.planningStructureWithType = planningStructureWithType
+ self.postalBox = postalBox
+ self.postalCode = postalCode
+ self.qc = qc
+ self.qcComplete = qcComplete
+ self.qcGeo = qcGeo
+ self.qcHouse = qcHouse
+ self.region = region
+ self.regionFiasId = regionFiasId
+ self.regionIsoCode = regionIsoCode
+ self.regionKladrId = regionKladrId
+ self.regionType = regionType
+ self.regionTypeFull = regionTypeFull
+ self.regionWithType = regionWithType
+ self.settlement = settlement
+ self.settlementFiasId = settlementFiasId
+ self.settlementKladrId = settlementKladrId
+ self.settlementType = settlementType
+ self.settlementTypeFull = settlementTypeFull
+ self.settlementWithType = settlementWithType
+ self.source = source
+ self.squareMeterPrice = squareMeterPrice
+ self.street = street
+ self.streetFiasId = streetFiasId
+ self.streetKladrId = streetKladrId
+ self.streetType = streetType
+ self.streetTypeFull = streetTypeFull
+ self.streetWithType = streetWithType
+ self.taxOffice = taxOffice
+ self.taxOfficeLegal = taxOfficeLegal
+ self.timezone = timezone
+ self.unparsedParts = unparsedParts
+ }
+
+ public init(from decoder: Decoder) throws {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ area = try values.decodeIfPresent(String.self, forKey: .area)
+ areaFiasId = try values.decodeIfPresent(String.self, forKey: .areaFiasId)
+ areaKladrId = try values.decodeIfPresent(String.self, forKey: .areaKladrId)
+ areaType = try values.decodeIfPresent(String.self, forKey: .areaType)
+ areaTypeFull = try values.decodeIfPresent(String.self, forKey: .areaTypeFull)
+ areaWithType = try values.decodeIfPresent(String.self, forKey: .areaWithType)
+ beltwayDistance = try values.decodeIfPresent(String.self, forKey: .beltwayDistance)
+ beltwayHit = try values.decodeIfPresent(String.self, forKey: .beltwayHit)
+ block = try values.decodeIfPresent(String.self, forKey: .block)
+ blockType = try values.decodeIfPresent(String.self, forKey: .blockType)
+ blockTypeFull = try values.decodeIfPresent(String.self, forKey: .blockTypeFull)
+ building = try values.decodeIfPresent(String.self, forKey: .building)
+ buildingType = try values.decodeIfPresent(String.self, forKey: .buildingType)
+ cadastralNumber = try values.decodeIfPresent(String.self, forKey: .cadastralNumber)
+ capitalMarker = try values.decodeIfPresent(String.self, forKey: .capitalMarker)
+ city = try values.decodeIfPresent(String.self, forKey: .city)
+ cityArea = try values.decodeIfPresent(String.self, forKey: .cityArea)
+ cityDistrict = try values.decodeIfPresent(String.self, forKey: .cityDistrict)
+ cityDistrictFiasId = try values.decodeIfPresent(String.self, forKey: .cityDistrictFiasId)
+ cityDistrictKladrId = try values.decodeIfPresent(String.self, forKey: .cityDistrictKladrId)
+ cityDistrictType = try values.decodeIfPresent(String.self, forKey: .cityDistrictType)
+ cityDistrictTypeFull = try values.decodeIfPresent(String.self, forKey: .cityDistrictTypeFull)
+ cityDistrictWithType = try values.decodeIfPresent(String.self, forKey: .cityDistrictWithType)
+ cityFiasId = try values.decodeIfPresent(String.self, forKey: .cityFiasId)
+ cityKladrId = try values.decodeIfPresent(String.self, forKey: .cityKladrId)
+ cityType = try values.decodeIfPresent(String.self, forKey: .cityType)
+ cityTypeFull = try values.decodeIfPresent(String.self, forKey: .cityTypeFull)
+ cityWithType = try values.decodeIfPresent(String.self, forKey: .cityWithType)
+ country = try values.decodeIfPresent(String.self, forKey: .country)
+ countryIsoCode = try values.decodeIfPresent(String.self, forKey: .countryIsoCode)
+ federalDistrict = try values.decodeIfPresent(String.self, forKey: .federalDistrict)
+ fiasActualityState = try values.decodeIfPresent(String.self, forKey: .fiasActualityState)
+ fiasCode = try values.decodeIfPresent(String.self, forKey: .fiasCode)
+ fiasId = try values.decodeIfPresent(String.self, forKey: .fiasId)
+ fiasLevel = try values.decodeIfPresent(String.self, forKey: .fiasLevel)
+ flat = try values.decodeIfPresent(String.self, forKey: .flat)
+ flatFiasId = try values.decodeIfPresent(String.self, forKey: .flatFiasId)
+ flatArea = try values.decodeIfPresent(String.self, forKey: .flatArea)
+ flatPrice = try values.decodeIfPresent(String.self, forKey: .flatPrice)
+ flatType = try values.decodeIfPresent(String.self, forKey: .flatType)
+ flatTypeFull = try values.decodeIfPresent(String.self, forKey: .flatTypeFull)
+ geoLat = try values.decodeIfPresent(String.self, forKey: .geoLat)
+ geoLon = try values.decodeIfPresent(String.self, forKey: .geoLon)
+ geonameId = try values.decodeIfPresent(String.self, forKey: .geonameId)
+ historyValues = try values.decodeIfPresent([String].self, forKey: .historyValues)
+ house = try values.decodeIfPresent(String.self, forKey: .house)
+ houseFiasId = try values.decodeIfPresent(String.self, forKey: .houseFiasId)
+ houseKladrId = try values.decodeIfPresent(String.self, forKey: .houseKladrId)
+ houseType = try values.decodeIfPresent(String.self, forKey: .houseType)
+ houseTypeFull = try values.decodeIfPresent(String.self, forKey: .houseTypeFull)
+ kladrId = try values.decodeIfPresent(String.self, forKey: .kladrId)
+ metro = try values.decodeIfPresent([Metro].self, forKey: .metro)
+ okato = try values.decodeIfPresent(String.self, forKey: .okato)
+ oktmo = try values.decodeIfPresent(String.self, forKey: .oktmo)
+ planningStructure = try values.decodeIfPresent(String.self, forKey: .planningStructure)
+ planningStructureFiasId = try values.decodeIfPresent(String.self, forKey: .planningStructureFiasId)
+ planningStructureKladrId = try values.decodeIfPresent(String.self, forKey: .planningStructureKladrId)
+ planningStructureType = try values.decodeIfPresent(String.self, forKey: .planningStructureType)
+ planningStructureTypeFull = try values.decodeIfPresent(String.self, forKey: .planningStructureTypeFull)
+ planningStructureWithType = try values.decodeIfPresent(String.self, forKey: .planningStructureWithType)
+ postalBox = try values.decodeIfPresent(String.self, forKey: .postalBox)
+ postalCode = try values.decodeIfPresent(String.self, forKey: .postalCode)
+
+ qc = values.decodeJSONNumber(forKey: CodingKeys.qc)
+ qcComplete = values.decodeJSONNumber(forKey: CodingKeys.qcComplete)
+ qcGeo = values.decodeJSONNumber(forKey: CodingKeys.qcGeo)
+ qcHouse = values.decodeJSONNumber(forKey: CodingKeys.qcHouse)
+
+ region = try values.decodeIfPresent(String.self, forKey: .region)
+ regionFiasId = try values.decodeIfPresent(String.self, forKey: .regionFiasId)
+ regionIsoCode = try values.decodeIfPresent(String.self, forKey: .regionIsoCode)
+ regionKladrId = try values.decodeIfPresent(String.self, forKey: .regionKladrId)
+ regionType = try values.decodeIfPresent(String.self, forKey: .regionType)
+ regionTypeFull = try values.decodeIfPresent(String.self, forKey: .regionTypeFull)
+ regionWithType = try values.decodeIfPresent(String.self, forKey: .regionWithType)
+ settlement = try values.decodeIfPresent(String.self, forKey: .settlement)
+ settlementFiasId = try values.decodeIfPresent(String.self, forKey: .settlementFiasId)
+ settlementKladrId = try values.decodeIfPresent(String.self, forKey: .settlementKladrId)
+ settlementType = try values.decodeIfPresent(String.self, forKey: .settlementType)
+ settlementTypeFull = try values.decodeIfPresent(String.self, forKey: .settlementTypeFull)
+ settlementWithType = try values.decodeIfPresent(String.self, forKey: .settlementWithType)
+ source = try values.decodeIfPresent(String.self, forKey: .source)
+ squareMeterPrice = try values.decodeIfPresent(String.self, forKey: .squareMeterPrice)
+ street = try values.decodeIfPresent(String.self, forKey: .street)
+ streetFiasId = try values.decodeIfPresent(String.self, forKey: .streetFiasId)
+ streetKladrId = try values.decodeIfPresent(String.self, forKey: .streetKladrId)
+ streetType = try values.decodeIfPresent(String.self, forKey: .streetType)
+ streetTypeFull = try values.decodeIfPresent(String.self, forKey: .streetTypeFull)
+ streetWithType = try values.decodeIfPresent(String.self, forKey: .streetWithType)
+ taxOffice = try values.decodeIfPresent(String.self, forKey: .taxOffice)
+ taxOfficeLegal = try values.decodeIfPresent(String.self, forKey: .taxOfficeLegal)
+ timezone = try values.decodeIfPresent(String.self, forKey: .timezone)
+ unparsedParts = try values.decodeIfPresent(String.self, forKey: .unparsedParts)
+ }
+ }
+}
diff --git a/Sources/IIDadata/Model/Address/AddressSuggestionQuery.swift b/Sources/IIDadata/Model/Address/AddressSuggestionQuery.swift
new file mode 100644
index 0000000..25e1f64
--- /dev/null
+++ b/Sources/IIDadata/Model/Address/AddressSuggestionQuery.swift
@@ -0,0 +1,191 @@
+//
+// AddressSuggestionQuery.swift
+// IIDadata
+//
+// Created by Yachin Ilya on 12.05.2020.
+//
+
+import Foundation
+
+// MARK: - AddressSuggestionQuery
+
+/// `AddressSuggestionQuery` represents an serializable object used to perform certain queries.
+public class AddressSuggestionQuery: Encodable, DadataQueryProtocol {
+ // Nested Types
+
+ enum CodingKeys: String, CodingKey {
+ case query
+ case resultsCount = "count"
+ case language
+ case constraints = "locations"
+ case regionPriority = "locations_boost"
+ case upperScaleLimit = "from_bound"
+ case lowerScaleLimit = "to_bound"
+ case trimRegionResult = "restrict_value"
+ }
+
+ // Properties
+
+ public var resultsCount: Int? = 10
+ public var language: QueryResultLanguage?
+ public var constraints: [AddressQueryConstraint]?
+ public var regionPriority: [RegionPriority]?
+ public var upperScaleLimit: ScaleBound?
+ public var lowerScaleLimit: ScaleBound?
+ public var trimRegionResult: Bool = false
+
+ let query: String
+ let queryType: AddressQueryType
+
+ // Lifecycle
+
+ /// New instance of AddressSuggestionQuery defaulting to simple address suggestions request.
+ /// - Parameter query: Query string to be sent to API.
+ public convenience init(_ query: String) {
+ self.init(query, ofType: .address)
+ }
+
+ /// New instance of AddressSuggestionQuery.
+ /// - Parameter query: Query string to be sent to API.
+ /// - Parameter ofType: Type of request to send to API.
+ /// It could be of type
+ /// `address` — standart address suggestion query;
+ /// `fiasOnly` — query to only search in FIAS database: less matches, state provided address data only;
+ /// `findByID` — takes KLADR or FIAS ID as a query parameter to lookup additional data.
+ public required init(_ query: String, ofType type: AddressQueryType) {
+ self.query = query
+ queryType = type
+ }
+
+ // Functions
+
+ /// Serializes AddressSuggestionQuery to send over the wire.
+ func toJSON() throws -> Data {
+ if constraints?.isEmpty ?? false { constraints = nil }
+ if regionPriority?.isEmpty ?? false { regionPriority = nil }
+ if let upper = upperScaleLimit,
+ let lower = lowerScaleLimit,
+ upper.value == nil || lower.value == nil
+ {
+ upperScaleLimit = nil
+ lowerScaleLimit = nil
+ }
+ return try JSONEncoder().encode(self)
+ }
+
+ /// Returns an API endpoint for different request types:
+ /// `address` — "suggest/address"
+ /// `fiasOnly` — "suggest/fias"
+ /// `findByID` — "findById/address"
+ func queryEndpoint() -> String { return queryType.rawValue }
+}
+
+// MARK: - ScaleLevel
+
+/// Levels of `from_bound` and `to_bound` according to
+/// [Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=285343795).
+public enum ScaleLevel: String, Encodable {
+ case country
+ case region
+ case area
+ case city
+ case settlement
+ case street
+ case house
+ case flat
+}
+
+// MARK: - AddressQueryConstraint
+/// AddressQueryConstraint used to limit search results according to
+/// [Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=204669108).
+@available(iOS 14.0, *)
+public struct AddressQueryConstraint: Codable {
+ // Properties
+
+ public var region: String?
+ public var city: String?
+ public var street_type_full: String?
+ public var settlement_type_full: String?
+ public var city_district_type_full: String?
+ public var city_type_full: String?
+ public var area_type_full: String?
+ public var region_type_full: String?
+ public var country: String?
+ public var country_iso_code: String?
+ public var region_iso_code: String?
+ public var kladr_id: String?
+ public var region_fias_id: String?
+ public var area_fias_id: String?
+ public var city_fias_id: String?
+ public var settlement_fias_id: String?
+ public var street_fias_id: String?
+
+ // Lifecycle
+
+ public init(
+ region: String? = nil,
+ city: String? = nil,
+ street_type_full: String? = nil,
+ settlement_type_full: String? = nil,
+ city_district_type_full: String? = nil,
+ city_type_full: String? = nil,
+ area_type_full: String? = nil,
+ region_type_full: String? = nil,
+ country: String? = nil,
+ country_iso_code: String? = nil,
+ region_iso_code: String? = nil,
+ kladr_id: String? = nil,
+ region_fias_id: String? = nil,
+ area_fias_id: String? = nil,
+ city_fias_id: String? = nil,
+ settlement_fias_id: String? = nil,
+ street_fias_id: String? = nil
+ ) {
+ self.region = region
+ self.city = city
+ self.street_type_full = street_type_full
+ self.settlement_type_full = settlement_type_full
+ self.city_district_type_full = city_district_type_full
+ self.city_type_full = city_type_full
+ self.area_type_full = area_type_full
+ self.region_type_full = region_type_full
+ self.country = country
+ self.country_iso_code = country_iso_code
+ self.region_iso_code = region_iso_code
+ self.kladr_id = kladr_id
+ self.region_fias_id = region_fias_id
+ self.area_fias_id = area_fias_id
+ self.city_fias_id = city_fias_id
+ self.settlement_fias_id = settlement_fias_id
+ self.street_fias_id = street_fias_id
+ }
+}
+
+// MARK: - RegionPriority
+
+/// Helps prioritize specified region in search results by KLADR ID.
+public struct RegionPriority: Encodable {
+ // Properties
+
+ public var kladr_id: String?
+
+ // Lifecycle
+
+ public init(kladr_id: String?) { self.kladr_id = kladr_id }
+}
+
+// MARK: - ScaleBound
+
+/// ScaleBound holds a value for `from_bound` and `to_bound` as a ScaleLevel.
+/// See
+/// [Dadata online API documentation](https://confluence.hflabs.ru/pages/viewpage.action?pageId=285343795) for API reference.
+public struct ScaleBound: Encodable {
+ // Properties
+
+ public var value: ScaleLevel?
+
+ // Lifecycle
+
+ public init(value: ScaleLevel?) { self.value = value }
+}
+
diff --git a/Sources/IIDadata/Model/Address/AddressSuggestionResponse.swift b/Sources/IIDadata/Model/Address/AddressSuggestionResponse.swift
new file mode 100644
index 0000000..5c3c188
--- /dev/null
+++ b/Sources/IIDadata/Model/Address/AddressSuggestionResponse.swift
@@ -0,0 +1,41 @@
+//
+// Models Generated using http://www.jsoncafe.com/
+// Created on May 11, 2020
+
+import Foundation
+
+// MARK: - AddressSuggestionResponse
+
+/// AddressSuggestionResponse represents a deserializable object used to hold API response.
+public struct AddressSuggestionResponse: Decodable, Sendable {
+ public let suggestions: [AddressSuggestion]?
+}
+
+// MARK: - Metro
+
+/// Structure holding metro station name, name of a line and distance to suggested address.
+/// If there aren't metro stations nearby or API token used not subscribed to "Maximal" package
+/// `nil` is returned instead.
+public struct Metro: Decodable, Sendable {
+ // Nested Types
+
+ enum CodingKeys: String, CodingKey {
+ case name, line, distance
+ }
+
+ // Properties
+
+ public let name: String?
+ public let line: String?
+ public let distance: String?
+
+ // Lifecycle
+
+ public init(from decoder: Decoder) throws {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ name = try values.decodeIfPresent(String.self, forKey: .name)
+ line = try values.decodeIfPresent(String.self, forKey: .line)
+
+ distance = values.decodeJSONNumber(forKey: CodingKeys.distance)
+ }
+}
diff --git a/Sources/IIDadata/Model/Address/ReverseGeocodeQuery.swift b/Sources/IIDadata/Model/Address/ReverseGeocodeQuery.swift
new file mode 100644
index 0000000..ab55bdb
--- /dev/null
+++ b/Sources/IIDadata/Model/Address/ReverseGeocodeQuery.swift
@@ -0,0 +1,75 @@
+//
+// ReverseGeocodeQuery.swift
+// IIDadata
+//
+// Created by Yachin Ilya on 12.05.2020.
+//
+
+import Foundation
+
+// MARK: - ReverseGeocodeQuery
+
+/// ReverseGeocodeQuery represents an serializable object used to perform reverse geocode queries.
+public class ReverseGeocodeQuery: Encodable, DadataQueryProtocol {
+ // Nested Types
+
+ enum CodingKeys: String, CodingKey {
+ case latitude = "lat"
+ case longitude = "lon"
+ case resultsCount = "count"
+ case language
+ case searchRadius = "radius_meters"
+ }
+
+ // Properties
+
+ public var resultsCount: Int? = 10
+ public var language: QueryResultLanguage?
+ public var searchRadius: Int?
+
+ let latitude: Double
+ let longitude: Double
+ let endpoint: String
+
+ // Lifecycle
+
+ /// New instance of ReverseGeocodeQuery.
+ /// - Parameter query: Query should contain latitude and longitude of the point of interest.
+ /// - Parameter delimeter: Single character delimeter to separate latitude and longitude.
+ /// - Throws: May throw if parsing of latitude and longitude out of query fails.
+ public convenience init(query: String, delimeter: Character = ",") throws {
+ let splitStr = query.split(separator: delimeter)
+
+ let latStr = String(splitStr[0]).trimmingCharacters(in: .whitespacesAndNewlines)
+ let lonStr = String(splitStr[1]).trimmingCharacters(in: .whitespacesAndNewlines)
+
+ guard let latitude = Double(latStr),
+ let longitude = Double(lonStr)
+ else {
+ throw NSError(domain: "Dadata ReverseGeocodeQuery",
+ code: -1,
+ userInfo: ["description": "Failed to parse coordinates from \(query) using delimiter \(delimeter)"])
+ }
+
+ self.init(latitude: latitude, longitude: longitude)
+ }
+
+ /// New instance of ReverseGeocodeQuery.
+ /// - Parameter latitude: Latitude of the point of interest.
+ /// - Parameter longitude: Longitude of the point of interest.
+ public required init(latitude: Double, longitude: Double) {
+ self.latitude = latitude
+ self.longitude = longitude
+ endpoint = Constants.revGeocodeEndpoint
+ }
+
+ // Functions
+
+ /// Serializes ReverseGeocodeQuery to send over the wire.
+ func toJSON() throws -> Data {
+ return try JSONEncoder().encode(self)
+ }
+
+ /// Returns an API endpoint for reverse geocode query.
+ func queryEndpoint() -> String { return endpoint }
+}
diff --git a/Sources/IIDadata/Model/Fio/FioData.swift b/Sources/IIDadata/Model/Fio/FioData.swift
new file mode 100644
index 0000000..755c1ac
--- /dev/null
+++ b/Sources/IIDadata/Model/Fio/FioData.swift
@@ -0,0 +1,136 @@
+//
+// FioData.swift
+// IIDadata
+//
+// Created by NSFuntik on 09.08.2024.
+//
+
+import Foundation
+
+public extension FioSuggestion {
+ // MARK: - FioData
+
+ /// A structure representing detailed FIO data.
+ ///
+ /// - Parameters:
+ /// - surname: Фамилия
+ /// - name: Имя
+ /// - patronymic: Отчество
+ /// - gender: Пол
+ /// - qc: Код качества
+ /// - source: Не заполняется
+ struct FioData: Codable, Equatable, Hashable {
+ // Nested Types
+
+ public enum CodingKeys: String, CodingKey {
+ case surname, name, patronymic, gender, qc
+
+ // Computed Properties
+
+ public var partQueryValue: String {
+ switch self {
+ case .surname: return "SURNAME"
+ case .name: return "NAME"
+ case .patronymic: return "PATRONYMIC"
+ default: return ""
+ }
+ }
+ }
+
+ // Properties
+
+ /// The surname (last name).
+ public let surname: String?
+
+ /// The given name (first name).
+ public let name: String?
+
+ /// The patronymic (middle name).
+ public let patronymic: String?
+
+ /// The gender of the individual.
+ public let gender: Gender?
+
+ /// The quality code.
+ ///
+ /// - `0`: если все части ФИО найдены в справочниках.
+ /// - `1`: если в ФИО есть часть не из справочника
+ /// - `null`: если нет части ФИО в справочниках.
+ public let qc: String?
+
+ // Lifecycle
+
+ /// Initializes a new FioData.
+ ///
+ /// - Parameters:
+ /// - surname: The surname (last name).
+ /// - name: The given name (first name).
+ /// - patronymic: The patronymic (middle name).
+ /// - gender: The gender of the individual.
+ /// - qc: The quality code.
+ public init(
+ surname: String?,
+ name: String?,
+ patronymic: String?,
+ gender: Gender? = nil,
+ qc: String? = nil
+ ) {
+ self.surname = surname
+ self.name = name
+ self.patronymic = patronymic
+ self.gender = gender
+ self.qc = qc
+ }
+
+ // MARK: - Codable
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ surname = try container.decodeIfPresent(String.self, forKey: .surname)
+ name = try container.decodeIfPresent(String.self, forKey: .name)
+ patronymic = try container.decodeIfPresent(String.self, forKey: .patronymic)
+ gender = try Gender(rawValue: container.decodeIfPresent(Gender.RawValue.self, forKey: .gender) ?? Gender.unknown.rawValue)
+ qc = try container.decodeIfPresent(String.self, forKey: .qc)
+ }
+
+ // Static Functions
+
+ // MARK: - Comparable
+
+ public static func < (lhs: FioData, rhs: FioData) -> Bool {
+ return (lhs.qc ?? "0") < (rhs.qc ?? "0") && lhs.qc != rhs.qc
+ }
+
+ // Functions
+
+ /// Returns the full name of the `FioData`.
+ public func fullName() -> String {
+ return [surname, name, patronymic].compactMap { $0 }.joined(separator: " ")
+ }
+
+ // MARK: - Hashable
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(surname)
+ hasher.combine(name)
+ hasher.combine(patronymic)
+ hasher.combine(gender)
+ hasher.combine(qc)
+ }
+
+ public subscript(_ key: CodingKeys) -> String? {
+ switch key {
+ case .surname:
+ return surname
+ case .name:
+ return name
+ case .patronymic:
+ return patronymic
+ case .gender:
+ return gender?.rawValue
+ case .qc:
+ return qc.map { String($0) }
+ }
+ }
+ }
+}
diff --git a/Sources/IIDadata/Model/Fio/FioSuggestion.swift b/Sources/IIDadata/Model/Fio/FioSuggestion.swift
new file mode 100644
index 0000000..f388d98
--- /dev/null
+++ b/Sources/IIDadata/Model/Fio/FioSuggestion.swift
@@ -0,0 +1,76 @@
+//
+// FioSuggestion.swift
+// IIDadata
+//
+// Created by NSFuntik on 09.08.2024.
+//
+
+// MARK: - FioSuggestion
+
+/// Every single suggestion is represented as `FioSuggestion`.
+/// - Parameters:
+/// - value` : ФИО одной строкой
+/// - unrestricted_value` : == value
+/// - data: Detailed FIO data structure.
+///
+///
+/// All the data returned in response to suggestion query.
+///
+/// - SeeAlso: ``FioData``
+///
+public struct FioSuggestion: Encodable, Suggestion {
+ // Nested Types
+
+ enum CodingKeys: String, CodingKey {
+ case value
+ case unrestrictedValue = "unrestricted_value"
+ case data
+ }
+
+ // Properties
+
+ /// Detailed FIO data.
+ public let data: FioData?
+
+ /// The suggested FIO value.
+ public let value: String
+
+ /// The unrestricted suggested FIO value. (` == value `)
+ public let unrestrictedValue: String?
+
+ // Lifecycle
+
+ /// Initializes a new FioSuggestion.
+ ///
+ /// - Parameters:
+ /// - value: The suggested FIO value.
+ /// - unrestrictedValue: The unrestricted suggested FIO value.
+ /// - data: Detailed FIO data.
+ public init(
+ _ value: String,
+ unrestrictedValue: String,
+ data: FioData
+ ) {
+ self.value = value
+ self.unrestrictedValue = unrestrictedValue
+ self.data = data
+ }
+
+ public init(from decoder: Decoder) throws {
+ do {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ value = try values.decode(String.self, forKey: .value)
+ unrestrictedValue = try values.decodeIfPresent(String.self, forKey: .unrestrictedValue)
+ data = try values.decodeIfPresent(FioData.self, forKey: .data)
+ } catch {
+ dump(error, name: "FioSuggestion Decoding Error")
+ throw error
+ }
+ }
+
+ // Functions
+
+ public subscript(_ key: FioData.CodingKeys) -> String? {
+ return data?[key]
+ }
+}
diff --git a/Sources/IIDadata/Model/Fio/FioSuggestionQuery.swift b/Sources/IIDadata/Model/Fio/FioSuggestionQuery.swift
new file mode 100644
index 0000000..03f0e87
--- /dev/null
+++ b/Sources/IIDadata/Model/Fio/FioSuggestionQuery.swift
@@ -0,0 +1,148 @@
+//
+// FioSuggestionQuery.swift
+// IIDadata
+//
+// Created by NSFuntik on 07.08.2024.
+//
+
+// MARK: - FIO Suggestion Query and Response Structures
+
+import Foundation
+
+// MARK: - FioSuggestionQuery
+
+/** Подсказки по ФИО (API)
+ # Parameters:
+ - query: да Запрос, для которого нужно получить подсказки
+ - count: Количество возвращаемых подсказок (по умолчанию — 10, максимум — 20).
+ - parts: Подсказки по части ФИО
+ - gender: Пол (UNKNOWN / MALE / FEMALE) (``Gender``)
+
+ # Description:
+ Помогает человеку быстро ввести ФИО на веб-форме или в приложении.
+
+ ## Что умеет:
+ - Подсказывает ФИО одной строкой или отдельно фамилию, имя, отчество.
+ - Исправляет клавиатурную раскладку («fynjy» → «Антон»).
+ - Определяет пол.
+
+ ## Не умеет:
+ - ❌ Автоматически (без участия человека) обработать ФИО из базы или файла.
+ - ❌ Транслитерировать (Juliia Somova → Юлия Сомова).
+ - ❌ Склонять по падежам (кого? кому? кем?).
+
+ ## Примечания:
+ Подсказки не подходят для автоматической обработки ФИО. Они предлагают варианты, но не гарантируют, что угадали правильно. Поэтому окончательное решение всегда должен принимать человек.
+
+ */
+public struct FioSuggestionQuery: Codable, DadataQueryProtocol {
+ // Nested Types
+
+ public enum Part: String, RawRepresentable, CodingKey, Codable {
+ case name = "NAME", patronymic = "PATRONYMIC", surname = "SURNAME"
+
+ // Computed Properties
+
+ public var rawValue: String {
+ switch self {
+ case .name: return "NAME"
+ case .patronymic: return "PATRONYMIC"
+ case .surname: return "SURNAME"
+ }
+ }
+
+ // Lifecycle
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let value = try container.decode(String.self)
+ self = Part(rawValue: value) ?? .name
+ }
+
+ public init?(rawValue: String) {
+ switch rawValue.uppercased() {
+ case "NAME": self = .name
+ case "PATRONYMIC": self = .patronymic
+ case "SURNAME": self = .surname
+ default: return nil
+ }
+ }
+
+ // Functions
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ try container.encode(rawValue)
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(rawValue)
+ }
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case query, count, parts, gender
+ }
+
+ // Properties
+
+ /// The search query string.
+ let query: String
+
+ /// The number of suggestions to return (default is 10).
+ let count: IntegerLiteralType?
+
+ /// Indicates if the name parts should be returned separately. Default is `false`.
+ let parts: [Part]?
+
+ /// Requested gender for the suggested names.
+ let gender: Gender?
+
+ // Lifecycle
+
+ /// Initializes a new `FioSuggestionQuery`.
+ ///
+ /// - Parameters:
+ /// - query: The search query string.
+ /// - count: The number of suggestions to return (default is 10).
+ /// - parts: Indicates if the name parts should be returned separately (default is `false`).
+ /// - gender: Requested gender for the suggested names.
+ public init(
+ _ query: String,
+ count: Int? = 10,
+ parts: [Part]? = nil,
+ gender: Gender? = nil
+ ) {
+ self.query = query
+ self.count = count
+ self.parts = parts
+ self.gender = gender
+ }
+
+ // Functions
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(query, forKey: .query)
+ try container.encodeIfPresent(count, forKey: .count)
+ try container.encodeIfPresent(gender?.codingKey, forKey: .gender)
+ if let parts = parts?.compactMap(\.rawValue), !parts.isEmpty {
+ try container.encodeIfPresent(parts, forKey: .parts)
+ }
+ }
+
+ /// Returns the endpoint for the query.
+ ///
+ /// - Returns: The endpoint as a string.
+ func queryEndpoint() -> String {
+ return "suggest/fio"
+ }
+
+ /// Encodes the query into a JSON data representation.
+ ///
+ /// - Returns: A `Data` object containing the JSON representation of the query.
+ /// - Throws: An error if the encoding fails.
+ func toJSON() throws -> Data {
+ try JSONEncoder().encode(self)
+ }
+}
diff --git a/Sources/IIDadata/Model/Fio/FioSuggestionResponse.swift b/Sources/IIDadata/Model/Fio/FioSuggestionResponse.swift
new file mode 100644
index 0000000..40fe198
--- /dev/null
+++ b/Sources/IIDadata/Model/Fio/FioSuggestionResponse.swift
@@ -0,0 +1,83 @@
+//
+// FioSuggestionResponse.swift
+// IIDadata
+//
+// Created by NSFuntik on 09.08.2024.
+//
+
+import Foundation
+
+// MARK: - FioSuggestionResponse
+
+/// A structure representing the response of a FIO suggestion query.
+public struct FioSuggestionResponse: Codable {
+ // Nested Types
+
+ enum CodingKeys: String, CodingKey {
+ case suggestions
+ }
+
+ // Properties
+
+ /// An array of FIO suggestions.
+ let suggestions: [FioSuggestion]
+
+ // Lifecycle
+
+ /// Initializes a new FioSuggestionResponse.
+ ///
+ /// - Parameters:
+ /// - suggestions: An array of FIO suggestions.
+ public init(suggestions: [FioSuggestion]) {
+ self.suggestions = suggestions
+ }
+
+ /// Initializes a new instance from the given decoder.
+ ///
+ /// - Parameter decoder: The decoder to read data from.
+ /// - Throws: An error if the decoding fails.
+ public init(from decoder: Decoder) throws {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ suggestions = try values.decode([FioSuggestion].self, forKey: .suggestions)
+ }
+
+ // Functions
+
+ /// Encodes the current instance into the given encoder.
+ ///
+ /// - Parameter encoder: The encoder to write data to.
+ /// - Throws: An error if the encoding fails.
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(suggestions, forKey: .suggestions)
+ }
+
+ /// Encodes the response into a JSON data representation.
+ ///
+ /// - Returns: A `Data` object containing the JSON representation of the response.
+ /// - Throws: An error if the encoding fails.
+ func toJSON() throws -> Data {
+ return try JSONEncoder().encode(self)
+ }
+
+ /// Returns the endpoint for querying.
+ ///
+ /// - Returns: The endpoint as a string.
+ func queryEndpoint() -> String {
+ return "suggest/fio"
+ }
+
+ /// Returns the type of the query.
+ ///
+ /// - Returns: The query type as a string.
+ func queryType() -> String {
+ return "fio"
+ }
+
+ /// Returns the number of suggestions in the response.
+ ///
+ /// - Returns: The count of suggestions.
+ func resultsCount() -> Int {
+ return suggestions.count
+ }
+}
diff --git a/Sources/IIDadata/Model/Fio/Gender.swift b/Sources/IIDadata/Model/Fio/Gender.swift
new file mode 100644
index 0000000..083f7ba
--- /dev/null
+++ b/Sources/IIDadata/Model/Fio/Gender.swift
@@ -0,0 +1,85 @@
+//
+// has.swift
+// IIDadata
+//
+// Created by NSFuntik on 09.08.2024.
+//
+
+import Foundation
+// MARK: - Gender
+
+/**
+ An enumeration representing gender with support for encoding and decoding,
+ equatability, and hashability.
+ The `Gender` enum has cases for male, female, and unknown genders, with
+ corresponding Russian strings for each case.
+ It also has an inner `CodingKeys` enumeration for mapping JSON keys to
+ enum cases and an initializer for decoding from a decoder.
+
+ # Parameters:
+ - male: The case representing the `male` gender
+ - female: The case representing the `female` gender
+ - unknown: The case representing an `unknown` gender
+
+ - SeeAlso: ``FioSuggestionResponse``, ``FioSuggestionQuery``
+
+ */
+public enum Gender: String, Codable, Equatable, Hashable, CaseIterable {
+ public static let allCases: [Gender] = [.male, .female]
+ /// The case representing the `male` gender with a Russian string value.
+ case male = "Мужской"
+ /// The case representing the `female` gender with a Russian string value.
+ case female = "Женский"
+ /// The case representing an `unknown` gender with a Russian string value.
+ case unknown = "–"
+
+ // Nested Types
+
+ /// An enumeration for defining the coding keys used for decoding.
+ public enum CodingKeys: String, CodingKey {
+ case male = "MALE"
+ case female = "FEMALE"
+ case unknown = "UNKNOWN"
+ }
+
+ // Computed Properties
+
+ /// The string value corresponding to the `Gender` case.
+ public var codingKey: String {
+ switch self {
+ case .male: return CodingKeys.male.rawValue
+ case .female: return CodingKeys.female.rawValue
+ case .unknown: return CodingKeys.unknown.rawValue
+ }
+ }
+
+ // Lifecycle
+
+ /// The `Gender` case corresponding to the given string value.
+ public init(rawValue: String) {
+ switch rawValue {
+ case "Мужской", "MALE": self = .male
+ case "Женский", "FEMALE": self = .female
+ default: self = .unknown
+ }
+ }
+
+ /**
+ Initializes a `Gender` instance from the given decoder.
+ This initializer attempts to decode a string from the specified container
+ using the `unknown` coding key. Based on the decoded string, it sets
+ the appropriate `Gender` case.
+
+ - Parameter decoder: The decoder to initialize the instance from.
+ - Throws: An error if decoding fails.
+ */
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ switch try container.decode(String.self, forKey: .unknown) {
+ case "MALE": self = .male
+ case "FEMALE": self = .female
+ default: self = .unknown
+ }
+ }
+}
+
diff --git a/Sources/IIDadata/Model/SuggestionProtocol.swift b/Sources/IIDadata/Model/SuggestionProtocol.swift
new file mode 100644
index 0000000..f086b83
--- /dev/null
+++ b/Sources/IIDadata/Model/SuggestionProtocol.swift
@@ -0,0 +1,95 @@
+//
+// Suggestion.swift
+// IIDadata
+//
+// Created by NSFuntik on 09.08.2024.
+//
+import SwiftUI
+
+// MARK: - Suggestion
+
+/**
+ Автодополнение при вводе («подсказки»)\
+ Протокол, которыйпредставляет собой десериализуемый объект, используемый для хранения ответа [DaData ](https://dadata.ru/) API
+
+ - Parameter value: текст подсказки одной строкой.
+ - Parameter unrestrictedValue: дополнительное поле для текста подсказки.
+ - Important: Поля не предназначены для автоматических интеграций, потому что их формат может со временем измениться
+
+ > Пример как заполняются поля для разных справочников:
+ > - ``AddressSuggestion``: Помогает человеку быстро ввести корректный адрес на веб-форме или в приложении.
+ > - ``FioSuggestion``: Помогает человеку быстро ввести ФИО на веб-форме или в приложении
+ - SeeAlso: [DaData API documentation](https://dadata.ru/api/suggest/)
+ */
+public protocol Suggestion: Decodable, Equatable, Hashable, Identifiable, Sendable {
+ typealias Value = String
+ associatedtype Data: Decodable
+ var unrestrictedValue: Value? { get }
+ var value: Value { get }
+ var data: Data? { get }
+ var type: SuggestionType { get }
+}
+
+public extension Suggestion where Self == FioSuggestion {
+ /// A computed property that returns the suggestion type for Fio suggestion, which is `.fio`
+ var type: SuggestionType { .fio }
+}
+
+public extension Suggestion where Self == AddressSuggestion {
+ /// A computed property that returns the suggestion type for Address suggestion, which is `.address`
+ var type: SuggestionType { .address }
+}
+
+public extension Suggestion {
+ /// Compares two Suggestion instances for equality based on their `value` property.
+ ///
+ /// - Parameters:
+ /// - lhs: The left-hand side `Suggestion` instance.
+ /// - rhs: The right-hand side `Suggestion` instance.
+ /// - Returns: A Boolean value indicating whether the two instances are equal.
+ static func == (lhs: Self, rhs: Self) -> Bool {
+ lhs.value == rhs.value
+ }
+
+ /// Hashes the essential components of the `Suggestion` instance by combining the `value` property.
+ ///
+ /// - Parameter hasher: The hasher to use when combining the components of this instance.
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(value)
+ }
+
+ /// A computed property that returns the ID of the `Suggestion` instance, which is the `value` property.
+ var id: String {
+ value
+ }
+
+ /// A computed property that returns a description of the `Suggestion` instance, which is the `value` property.
+ var description: String {
+ value
+ }
+}
+
+// MARK: - SuggestionType
+
+/**
+ An enumeration representing the types of suggestions available.
+ - `address`: Suggestion for an address.
+ - `fio`: Suggestion for a full name (first name, last name, etc.).
+ */
+public enum SuggestionType {
+ case address
+ case fio
+}
+
+// MARK: - IIDadataError
+
+/**
+ An enumeration representing possible errors that can occur while fetching suggestions.
+ - `noSuggestions`: Indicates that no suggestions were found.
+ */
+public enum IIDadataError: Error {
+ case noSuggestions
+ case invalidInput
+ case invalidResponse
+ case unknown(String)
+}
diff --git a/Sources/IIDadataUI/Suggestions Popover/DadataSuggestionsEnvironmentKey.swift b/Sources/IIDadataUI/Suggestions Popover/DadataSuggestionsEnvironmentKey.swift
new file mode 100644
index 0000000..be36bc4
--- /dev/null
+++ b/Sources/IIDadataUI/Suggestions Popover/DadataSuggestionsEnvironmentKey.swift
@@ -0,0 +1,90 @@
+//
+// DadataSuggestionsKey.swift
+// IIDadata
+//
+// Created by NSFuntik on 13.08.2024.
+//
+import IIDadata
+import SwiftUI
+
+// MARK: - View Extension
+
+public extension View {
+ /// A view modifier to display suggestions for the given input using `Dadata` API.
+ ///
+ /// This extension provides an easy way to apply the `IIDadataSuggestsPopover` view modifier to any `View`.
+ ///
+ /// - Parameters:
+ /// - apiKey: The API key for the `Dadata` API.
+ /// - text: A binding to the input text.
+ /// - suggestions: A binding to the list of suggestions.
+ /// - onSuggestionSelected: A closure to handle the selection of a suggestion.
+ ///
+ /// - Returns: A view with the `IIDadataSuggestsPopover` modifier applied.
+ @available(iOS 15.0, *) @ViewBuilder
+ func withDadataSuggestions(
+ isPresented: Binding = .constant(true),
+ apiKey: String,
+ input text: Binding,
+ suggestions: Binding<[S]?>,
+ textfieldHeight: CGFloat,
+ onSuggestionSelected: @escaping (S) -> Void
+ ) -> some View {
+ modifier(
+ IIDadataSuggestsPopover(
+ apiKey: apiKey,
+ input: text,
+ suggestions: suggestions,
+ textfieldHeight: textfieldHeight,
+ onSuggestionSelected: onSuggestionSelected
+ )
+ ).environment(\.dadataSuggestions, try? DadataSuggestions.shared(apiKey: apiKey))
+ }
+
+ /// A view modifier to display suggestions for the given input using `Dadata` API.
+ ///
+ /// This extension provides an easy way to apply the `IIDadataSuggestsPopover` view modifier to any `View`.
+ ///
+ /// - Parameters:
+ /// - apiKey: The API key for the `Dadata` API.
+ /// - text: A binding to the input text.
+ /// - suggestions: A binding to the list of suggestions.
+ /// - textfieldHeight: The height of the text field.
+ /// - onSuggestionSelected: A closure to handle the selection of a suggestion.
+ ///
+ /// - Returns: A view with the `IIDadataSuggestsPopover` modifier applied.
+ @available(iOS 15.0, *) @ViewBuilder
+ func withDadataSuggestions(
+ isPresented: Binding = .constant(true),
+ dadata: DadataSuggestions,
+ input text: Binding,
+ suggestions: Binding<[S]?>,
+ textfieldHeight: CGFloat,
+ onSuggestionSelected: @escaping (S) -> Void
+ ) -> some View {
+ modifier(
+ IIDadataSuggestsPopover(
+ input: text,
+ suggestions: suggestions,
+ textfieldHeight: textfieldHeight,
+ onSuggestionSelected: onSuggestionSelected
+ )
+ ).environment(\.dadataSuggestions, dadata)
+ }
+}
+
+extension EnvironmentValues {
+ /// The current `DadataSuggestions` instance.
+ @available(iOS 15.0, *)
+ var dadataSuggestions: DadataSuggestions? {
+ get { self[DadataSuggestionsKey.self] }
+ set { self[DadataSuggestionsKey.self] = newValue }
+ }
+}
+
+// MARK: - DadataSuggestionsKey
+
+/// An environment key that provides access to the current `DadataSuggestions` instance.
+struct DadataSuggestionsKey: EnvironmentKey {
+ static let defaultValue: DadataSuggestions? = nil
+}
diff --git a/Sources/IIDadataUI/Suggestions Popover/IIDadataSuggestionsPopover.swift b/Sources/IIDadataUI/Suggestions Popover/IIDadataSuggestionsPopover.swift
new file mode 100644
index 0000000..b2b2389
--- /dev/null
+++ b/Sources/IIDadataUI/Suggestions Popover/IIDadataSuggestionsPopover.swift
@@ -0,0 +1,435 @@
+//
+// IIDadataSuggestionsView.swift
+// IIDadata
+//
+// Created by NSFuntik on 11.08.2024.
+//
+import SwiftUI
+import IIDadata
+
+// MARK: - IIDadataSuggestsPopover
+
+/// A view modifier that provides a text field with suggestions as the user types.
+///
+/// The `IIDadataSuggestable` fetches suggestions for a `TextField`'s input and displays a list of suggestions, obtained asynchronously.
+/// When a suggestion is selected, it triggers an action `onSuggestionSelected`.
+///
+/// - Parameters:
+/// - apiKey: The API key for the `Dadata` API.
+/// - text: A binding to the input text.
+/// - suggestions: A binding to the list of suggestions.
+/// - onSuggestionSelected: A closure to handle the selection of a suggestion.
+///
+/// - Returns: A `View Modifier` that provides a text field with suggestions as the user types.
+///
+/// - Note: The `getSuggestions` function is called asynchronously and updates the `suggestions` property.
+///
+/// - SeeAlso: ``DadataSuggestions``
+@available(iOS 15.0, *)
+public struct IIDadataSuggestsPopover: ViewModifier {
+ /// The action to perform when a suggestion is selected.
+ /// - Parameter isPresentingPopover: An input parameter that indicates whether the popover is presented.
+ public typealias OnSuggestionSelected = (S) -> Void
+
+ /// The DadataSuggestions instance for fetching suggestions.
+ @Environment(\.dadataSuggestions) private var dadata
+
+ // Properties
+
+ /// The action to perform when a suggestion is selected.
+ public var onSuggestionSelected: OnSuggestionSelected
+
+ /// The TextField input text.
+ @Binding var text: String
+ /// The list of suggestions.
+ @Binding var suggestions: [S]?
+
+ let textfieldHeight: CGFloat
+
+ /// A binding to a boolean value that indicates whether the popover is presented.
+ @Binding var isPopoverPresented: Bool
+
+ private let viewID = UUID().uuidString
+
+ /// The error message.
+ @State private var error: String? = nil
+
+ @Namespace private var nsPopover
+
+ @FocusState private var isFocused: Bool
+
+ // Computed Properties
+
+ var idealHeight: CGFloat {
+ let suggestions = Double(self.suggestions?.endIndex ?? 1)
+ return textfieldHeight
+ + (UIFont.preferredFont(forTextStyle: .callout).lineHeight.scaled(by: suggestions))
+ }
+
+ var maxHeight: CGFloat {
+ idealHeight + 111
+ }
+
+ // Lifecycle
+
+ /// Initializes a new instance of a custom view with input text binding, an instance of `DadataSuggestions`,
+ /// suggestions binding, placeholder text, and a closure to handle suggestion selection.
+ ///
+ /// - Parameters:
+ /// - text: A binding to a text input.
+ /// - dadata: An instance of `DadataSuggestions` for fetching suggestions.
+ /// - suggestions: A binding to an optional array of suggestions of type `[T]`.
+ /// - placeholder: A string placeholder text.
+ /// - isPopoverPresented: A binding to a boolean value that indicates whether the popover is presented.
+ /// - onSuggestionSelected: A closure that gets executed when a suggestion is selected.
+ public init(
+ isPresented: Binding = .constant(true),
+ input text: Binding,
+ suggestions: Binding<[S]?>,
+ textfieldHeight: CGFloat,
+ onSuggestionSelected: @escaping (S) -> Void
+ ) where S: Suggestion {
+ _isPopoverPresented = isPresented
+ _text = text
+ _suggestions = suggestions
+ self.textfieldHeight = textfieldHeight
+ self.onSuggestionSelected = onSuggestionSelected
+ guard (try? DadataSuggestions.shared()) != nil else {
+ debugPrint("DaData service shared instanvce didn't configurated properly")
+ return
+ }
+ }
+
+ /// Initializes a new instance of a custom view with an API key, input text binding,
+ /// suggestions binding, and a closure to handle suggestion selection.
+ ///
+ /// - Parameters:
+ /// - apiKey: A string containing the API key for `DadataSuggestions`.
+ /// - text: A binding to a text input.
+ /// - suggestions: A binding to an optional array of suggestions of type `[T]`.
+ /// - isPresented: A binding to a boolean value that indicates whether the popover is presented.
+ /// - onSuggestionSelected: A closure that gets executed when a suggestion is selected.
+ public init(
+ isPresented: Binding = .constant(true),
+ apiKey _: String,
+ input text: Binding,
+ suggestions: Binding<[S]?>,
+ textfieldHeight: CGFloat,
+ onSuggestionSelected: @escaping (S) -> Void
+ ) where S: Suggestion {
+ _isPopoverPresented = isPresented
+ _text = text
+ _suggestions = suggestions
+ self.textfieldHeight = textfieldHeight + 6
+ self.onSuggestionSelected = onSuggestionSelected
+ }
+
+ // Content
+
+ @ViewBuilder public func body(content: Content) -> some View {
+ content
+ .coordinateSpace(name: nsPopover)
+ .autocorrectionDisabled()
+ .textInputAutocapitalization(.never)
+ .layoutPriority(1)
+ .zIndex(1)
+ .compositingGroup()
+ .padding(.bottom, isPopoverPresented && !(suggestions?.isEmpty ?? true) ? idealHeight : 0)
+ .background {
+ GeometryReader { geometry in
+ Color.clear
+ .matchedGeometryEffect(
+ id: viewID,
+ in: nsPopover,
+ properties: .frame,
+ anchor: .top,
+ isSource: true
+ )
+ .frame(height: idealHeight)
+ .offset(y: textfieldHeight / 2)
+ }
+ .overlay(alignment: .bottom) {
+ if let suggestions, !suggestions.isEmpty, isPopoverPresented {
+ popover()
+ .compositingGroup()
+ .matchedGeometryEffect(
+ id: viewID, in: nsPopover, properties: .frame, anchor: .top, isSource: false
+ )
+ .opacity(suggestions.isEmpty == true ? 0 : 1)
+ }
+ }
+ }
+ .onChange(of: text, perform: getSuggestions(for:))
+ .task(id: text) {
+ isPopoverPresented = true
+ getSuggestions(for: text)
+ }
+ .focused($isFocused).animation(.interactiveSpring, value: isFocused)
+ .onChange(of: isFocused) { isPopoverPresented = $0 }
+ .animation(.spring, value: isPopoverPresented)
+ .animation(.smooth, value: suggestions)
+ }
+
+ @ViewBuilder func popover() -> some View {
+ if isPopoverPresented, let suggestions = suggestions?.compactMap(\.self), !suggestions.isEmpty {
+ SuggestionsPopover(for: text, with: suggestions, height: textfieldHeight) { suggestion in
+ text = suggestion.value
+ onSuggestionSelected(suggestion)
+
+ isPopoverPresented = suggestions.count != 1
+ }
+ .background(.bar, in: .rect(cornerRadius: 10))
+ .frame(
+ minHeight: suggestions.endIndex > 1 ? 111 : 0,
+ idealHeight: idealHeight,
+ maxHeight: maxHeight,
+ alignment: .bottom
+ )
+ .tag(viewID, includeOptional: false)
+ .edgesIgnoringSafeArea(.all).ignoresSafeArea(.all)
+ .transition(.offset(x: 0, y: -idealHeight).animation(.spring))
+ }
+ }
+
+ // Functions
+
+ /// Fetches suggestions based on the input text.
+ ///
+ @MainActor private func getSuggestions(for input: String) {
+ Task { await getAsyncSuggestions(for: input) }
+ }
+
+ /// Fetches suggestions based on the input text.
+ ///
+ @MainActor @Sendable private func getAsyncSuggestions(for input: String) async {
+ // let input = text
+
+ guard !input.isEmpty else {
+ error = "Error fetching suggestions: \(IIDadataError.invalidInput)"
+ return
+ }
+
+ do {
+ switch S.self {
+ case is AddressSuggestion.Type:
+ debugPrint("AddressSuggestion – Fetching address suggestions for: \(input)")
+
+ let addressSuggestions = try await getAddressSuggestions(for: input) as! [S]
+ suggestions = addressSuggestions
+
+ case is FioSuggestion.Type:
+ debugPrint("FioSuggestion – Fetching Fio suggestions for: \(input)")
+
+ let fioSuggestions = try await getFioSuggestions() as! [S]
+ suggestions = fioSuggestions
+
+ default:
+ debugPrint("Unknown Suggestion – Fetching suggestions for: \(input)")
+ throw IIDadataError.unknown(String(describing: S.self))
+ }
+ } catch {
+ self.error = "Error fetching suggestions: \(error)"
+ suggestions = nil
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+extension IIDadataSuggestsPopover {
+ /// Fetches suggestions based on the input text.
+ ///
+ /// This asynchronous method fetches address or FIO (Full name) suggestions
+ /// based on the type of the first element in the `suggestions` array.
+ /// Fetches `FIO` (`Full Name`) suggestions based on the current FIO input.
+ /// It performs a check to ensure the FIO input is not empty before fetching suggestions.
+ ///
+ /// - Throws: ``IIDadataError`` if an error occurs while fetching suggestions.
+ func getFioSuggestions() async throws(IIDadata.IIDadataError)
+ -> [FioSuggestion] /* where S == IIDadata.FioSuggestion */
+ {
+ guard !text.isEmpty else {
+ suggestions = nil
+ throw IIDadataError.invalidInput
+ }
+ do {
+ let suggestions = try await DadataSuggestions.shared().suggestFio(
+ text,
+ count: 10,
+ gender: .male
+ )
+ dump(suggestions, name: "FIO Suggestion for: \(text)")
+ return suggestions as [FioSuggestion]
+ } catch let error as IIDadataError {
+ self.error = "Error fetching FIO suggestions: \(error)"
+ throw error
+ } catch {
+ self.error = "Error fetching FIO suggestions: \(error)"
+ throw IIDadataError.unknown(error.localizedDescription)
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+extension IIDadataSuggestsPopover {
+ /// Fetches suggestions based on the input text.
+ ///
+ /// This asynchronous method fetches address or FIO (Full name) suggestions
+ /// based on the type of the first element in the `suggestions` array.
+ /// If the text input is empty, it sets `suggestions` to `nil`.
+ ///
+ /// This function is called asynchronously and updates the `addressSuggestions` property.
+ /// It performs a check to ensure the address input is not empty before fetching suggestions.
+ /// - Throws: ``IIDadataError`` – an error object that indicates the type of error that occurred during the fetch process.
+ /// - Returns: An array of `AddressSuggestion` objects representing the fetched suggestions.
+ /// - SeeAlso: ``DadataSuggestions``
+ func getAddressSuggestions(for text: String) async throws(IIDadata.IIDadataError)
+ -> [AddressSuggestion] {
+ guard !text.isEmpty else {
+ suggestions = nil
+ throw IIDadataError.invalidInput
+ }
+ do {
+ guard
+ let suggestions = try await DadataSuggestions.shared().suggestAddress(
+ text,
+ queryType: .address,
+ resultsCount: 10,
+ language: .ru
+ ).suggestions else {
+ throw IIDadataError.noSuggestions
+ }
+
+ dump(suggestions, name: "Address Suggestion for: \(text)")
+ return suggestions as [AddressSuggestion]
+
+ } catch let error as IIDadataError {
+ self.error = "Error fetching Address Suggestions: \(error)"
+ throw error
+ } catch {
+ self.error = "Error fetching Address Suggestions: \(error)"
+ throw IIDadataError.unknown(error.localizedDescription)
+ }
+ }
+}
+
+// MARK: - Previews
+
+#if DEBUG
+
+ // MARK: - ContentView
+
+ /// A sample view demonstrating the usage of `IIDadataSuggestionsView`.
+ @available(iOS 15.0, *)
+ struct ContentView: View {
+ // Static Computed Properties
+
+ static var addressMock: AddressSuggestion {
+ .init(
+ value: "г. Санкт-Петербург, улица Грибалёвой, 7 к4 с1, кв. \(Int.random(in: 1 ... 333))",
+ data: .init(),
+ unrestrictedValue:
+ "г. Санкт-Петербург, улица Грибалёвой, 7 к4 с1, кв. \(Int.random(in: 1 ... 333))"
+ )
+ }
+
+ // Properties
+
+ @State var address = "Грибал"
+ @State var fio = "Михайл"
+ @State var addressSuggestions: [AddressSuggestion]? = [
+ addressMock, addressMock, addressMock, addressMock, addressMock, addressMock,
+ ]
+ @State var fioSuggestions: [FioSuggestion]? = nil
+ @State var error: String?
+
+ @StateObject private var dadata: DadataSuggestions
+
+ @FocusState private var isAddressFocused: Bool
+ @FocusState private var isFIOFocused: Bool
+
+ @State private var isAddressSuggestionsPresented = true
+ @State private var isFIOSuggestionsPresented = true
+
+ // Lifecycle
+
+ /// Initializes the `IIDadataViewModel` with the appropriate API key.
+ ///
+ /// The API key is fetched from the environment variables.
+ init() {
+ let apiKey = ProcessInfo.processInfo.environment["IIDadataAPIkey"] ?? ""
+ _dadata = StateObject(wrappedValue: try! DadataSuggestions.shared(apiKey: apiKey))
+ }
+
+ // Content
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 44) {
+ Spacer()
+ TextField(
+ "Enter address",
+ text: $address
+ )
+
+ .font(.body)
+ .textFieldStyle(.roundedBorder)
+ .tint(.blue).frame(height: 44)
+ .withDadataSuggestions(
+ isPresented: .constant(true),
+ dadata: dadata,
+ input: $address,
+ suggestions: $addressSuggestions,
+ textfieldHeight: 44,
+ onSuggestionSelected: {
+ address = $0.value
+ if let index = addressSuggestions?.firstIndex(where: { $0.value == address }) {
+ addressSuggestions?.remove(at: index)
+ } else {
+ addressSuggestions = nil
+ }
+ }
+ )
+ Spacer()
+ TextField(
+ "Enter Full Name",
+ text: $fio
+ )
+ .font(.body)
+ .textFieldStyle(.roundedBorder)
+ .tint(.blue).background(.white).frame(height: 56).clipped()
+ .withDadataSuggestions(
+ isPresented: .constant(true),
+ dadata: dadata,
+ input: $fio,
+ suggestions: $fioSuggestions,
+ textfieldHeight: 56,
+ onSuggestionSelected: {
+ fio = $0.value.appending(" ")
+ }
+ )
+
+ Spacer()
+ }
+ .padding()
+ }
+ .onAppear {
+ dump(dadata)
+ }
+ .background(
+ .conicGradient(
+ colors: [Color.pink, .accentColor, .teal, .purple, .brown, .mint, .indigo],
+ center: .center, angle: .degrees(.pi)
+ )
+ )
+ .ignoresSafeArea()
+ }
+ }
+
+ @available(iOS 15.0, *)
+ struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
+ }
+
+#endif
diff --git a/Sources/IIDadataUI/Suggestions Popover/SuggestionsPopover.swift b/Sources/IIDadataUI/Suggestions Popover/SuggestionsPopover.swift
new file mode 100644
index 0000000..ae0f2f7
--- /dev/null
+++ b/Sources/IIDadataUI/Suggestions Popover/SuggestionsPopover.swift
@@ -0,0 +1,162 @@
+//
+// SuggestionsPopover.swift
+// IIDadata
+//
+// Created by NSFuntik on 13.08.2024.
+//
+
+import IIDadata
+import SwiftUI
+
+// MARK: - IIDadataSuggestsPopover.SuggestionsPopover
+
+@available(iOS 15.0, *)
+public extension IIDadataSuggestsPopover {
+ // MARK: - SuggestionsPopover
+
+ /// A view that displays a list of suggestions.
+ ///
+ /// The `SuggestionsPopover` displays suggestions in a scrollable list and allows the user to select one.
+ ///
+ /// - Parameters:
+ /// - suggestions: An array of `Suggestion.Value` to be displayed.
+ /// - height: The height of `TextField`'s container for offseting the `popover`.
+ /// - inputText: `TextField's` input.
+ /// - onSelect: A closure that gets executed when a suggestion is selected.
+ /// - Returns: A `View` that displays a list of suggestions.
+ @available(iOS 15.0, *)
+ struct SuggestionsPopover: View {
+ // Properties
+
+ var suggestions: [S]
+ let onSelect: (S) -> Void
+ let textfieldHeight: CGFloat
+ @Namespace var nsNamespace
+ let maxWidth: CGFloat = UIScreen.main.bounds.width - 44
+
+ @State private var cachedInput: String = ""
+ @State private var cachedResults: [AttributedString] = []
+ private var inputText: String
+
+ // Computed Properties
+
+ /// The ideal height for the suggestions popover based on the number of suggestions.
+ var idealHeight: CGFloat {
+ let suggestions = self.suggestions.count
+ return textfieldHeight + (44.0 * CGFloat(suggestions > 1 ? suggestions : 1))
+ }
+
+ /// The maximum height for the suggestions popover.
+ var maxHeight: CGFloat {
+ idealHeight + 111
+ }
+
+ /// An array of highlighted suggestions based on the input text.
+ ///
+ /// This computed property caches the input text and its corresponding highlighted suggestions.
+ /// If the input text has not changed, it returns the cached results. Otherwise, it updates the cache and returns new results.
+ private var highlightedSuggestions: [AttributedString] {
+ if inputText == cachedInput {
+ return cachedResults
+ }
+ let newResults = suggestions.map { suggestion in
+ highlight(text: suggestion.value, match: inputText)
+ }
+ cachedInput = inputText
+ cachedResults = newResults
+ return newResults
+ }
+
+ // Lifecycle
+
+ /// Creates a new `SuggestionsPopover`.
+ ///
+ /// - Parameters:
+ /// - suggestions: An array of `Suggestion.Value` to be displayed.
+ /// - height: The height of `TextField`'s container for offseting the `popover`.
+ /// - inputText: `TextField's` input.
+ /// - onSelect: A closure that gets executed when a suggestion is selected.
+ init(
+ for inputText: String,
+ with suggestions: [S],
+ height textfieldHeight: CGFloat,
+ onSelect: @escaping (S) -> Void
+ ) {
+ self.inputText = inputText
+ self.suggestions = suggestions
+ self.onSelect = onSelect
+ self.textfieldHeight = textfieldHeight
+ UITableView.appearance().backgroundColor = .clear
+ UIScrollView.appearance().backgroundColor = .clear
+ }
+
+ // Content
+
+ public var body: some View {
+ ScrollView {
+ LazyVStack(spacing: 6) {
+ ForEach(suggestions, id: \.self, content: SelectedSuggestionView(_:))
+ }
+ .padding(8)
+ .listStyle(.inset)
+ }
+ .frame(
+ minWidth: 100,
+ idealWidth: UIScreen.main.bounds.width - 32,
+ maxWidth: UIScreen.main.bounds.width - 16,
+ minHeight: suggestions.endIndex > 1 ? 111 : 0,
+ idealHeight: idealHeight,
+ maxHeight: maxHeight,
+ alignment: .center
+ ).ignoresSafeArea(.all).edgesIgnoringSafeArea(.all)
+ .animation(.interactiveSpring, value: suggestions)
+ }
+
+ @ViewBuilder
+ func SelectedSuggestionView(_ suggestion: S) -> some View {
+ VStack(alignment: .leading, spacing: 5) {
+ ZStack(alignment: .leading, content: {
+ Button(action: {
+ onSelect(suggestion)
+ }) {
+ Text(highlight(text: suggestion.value, match: inputText))
+ .font(
+ .system(.callout, design: .rounded).weight(.light)
+ )
+ .lineLimit(1).allowsTightening(true)
+ .truncationMode(.head)
+ .foregroundStyle(.foreground)
+ .frame(minWidth: 166, maxWidth: maxWidth, alignment: .leading)
+ }
+ .buttonStyle(.borderless)
+ })
+ .accentColor(.accentColor)
+ Divider()
+ }.clipped(antialiased: true)
+ }
+
+ // Functions
+
+ /// Highlights the matching text within the provided text.
+ ///
+ /// This function takes the input text and highlights the portion that matches the provided match string.
+ /// The matching part of the text will have a different font weight.
+ ///
+ /// - Parameters:
+ /// - text: The text to be highlighted.
+ /// - match: The string to match and highlight within the text.
+ ///
+ /// - Returns: An `AttributedString` with the matched portion highlighted.
+ private func highlight(text: String, match: String) -> AttributedString {
+ var attributedString = AttributedString(text)
+ if let stringRange = text.range(of: match, options: .caseInsensitive) {
+ // Convert the String.Index range to an AttributedString.Index range
+ if let range = Range(stringRange, in: attributedString) {
+ attributedString[range].font = .callout.weight(.medium)
+ attributedString[range].foregroundColor = .primary
+ }
+ }
+ return attributedString
+ }
+ }
+}