diff --git a/.gitignore b/.gitignore
index c98f27fe..b2106bba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,11 @@ xcuserdata
*.xcuserstate
*.xcscmblueprint
+# Swift Package Manager
+.build/
+.swiftpm/
+Package.resolved
+
## Obj-C/Swift specific
*.hmap
*.ipa
diff --git a/.jazzy.yaml b/.jazzy.yaml
deleted file mode 100644
index 2486f9a7..00000000
--- a/.jazzy.yaml
+++ /dev/null
@@ -1,60 +0,0 @@
-xcodebuild_arguments:
- - -scheme
- - InstantSearch
-output: docs/reference
-# Avoid putting the DocSet within the HTML docs.
-# WARNING: The path is relative to the output directory.
-docset_path: ../../build/docset
-hide_documentation_coverage: true
-clean: true
-abstract:
- - LICENSE
-readme: docgen/jazzy/index.md
-custom_categories:
- - name: Core
- children:
- - InstantSearch
- - name: Widgets
- children:
- - ActivityIndicatorWidget
- - DatePickerWidget
- - HitsCollectionWidget
- - OneValueSwitchWidget
- - RefinementCollectionWidget
- - RefinementTableWidget
- - SearchBarWidget
- - SegmentedControlWidget
- - SliderWidget
- - StatsButtonWidget
- - StatsLabelWidget
- - StepperWidget
- - SwitchWidget
- - TextFieldWidget
- - TwoValuesSwitchWidget
- - name: Base ViewController
- children:
- - HitsCollectionViewController
- - HitsTableViewController
- - RefinementCollectionViewController
- - RefinementTableViewController
- - name: Controllers
- children:
- - HitsController
- - RefinementController
- - name: Protocols
- children:
- - AlgoliaWidget
- - HitsCollectionViewDataSource
- - HitsCollectionViewDelegate
- - HitsTableViewDataSource
- - HitsTableViewDelegate
- - RefinableDelegate
- - RefinementCollectionViewDataSource
- - RefinementCollectionViewDelegate
- - RefinementTableViewDataSource
- - RefinementTableViewDelegate
- - ResultingDelegate
- - SearchableViewModel
- - name: Helpers
- children:
- - UILabel
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 00000000..b6a65719
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,15 @@
+excluded:
+ - .build
+ - ./Tests
+# implicitly
+line_length: 300
+identifier_name:
+ excluded:
+ - on
+ - or
+ - id
+disabled_rules:
+ - todo
+ - implicit_getter
+nesting:
+ type_level: 2
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a6..00000000
--- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/InstantSearch.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/InstantSearch.xcscheme
deleted file mode 100644
index b23cab7c..00000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/InstantSearch.xcscheme
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index dd52f449..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-branches:
- only:
- - master
-osx_image: xcode9
-language: objective-c
-env:
- matrix:
- - FASTLANE_TARGET="ios"
-install: true
-before_script:
- - gem update fastlane
-script:
- - set -o pipefail
- - fastlane ${FASTLANE_TARGET} test
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index a323b16e..00000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,31 +0,0 @@
-Change Log
-==========
-## 2.1.0 (2017-12-12)
-
-- Multi-Index functionality added to all widgets with the introduction of the index and variant field.
-- Adding 2 new widgets: `MultiHitsTableWidget` and `MultiHitsCollectionWidget` that can handle multiple indices in different sections.
-- Introduction of 6 ViewModels that encapsulate the business logic of the widgets. You can use them to easily access the search state for better customization.
-`SearchViewModel`, `HitsViewModel`, `MultiHitsViewModel`, `NumericControlViewModel`, `FacetControlViewModel`, `RefinementMenuViewModel`.
-
-## 2.0.0 (2017-10-01)
-
-### Swift Version
-
-- Add support for Swift 4
-
-## 1.0.1 (2017-07-31)
-
-### Dependency Managers
-
-- Add support for Carthage
-
-## 1.0.0 (2017-07-17)
-
-**First official release of InstantSearch iOS!**
-
-### Features
-
-- 15 customizable widgets to use in your apps. Checkout the documentation of those widgets in the [community website](https://community.algolia.com/instantsearch-ios/widgets.html).
-- 4 base controllers: `HitsTableViewController`, `RefinementTableViewController`, `HitsCollectionViewController`, `RefinementCollectionViewController`.
-- Custom widget creation. [Follow documentation](https://community.algolia.com/instantsearch-ios/widgets.html#custom-widgets).
-- Getting Started Guide. [Follow guide](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/).
diff --git a/Cartfile b/Cartfile
index 2e7624bb..ce040c27 100644
--- a/Cartfile
+++ b/Cartfile
@@ -1,2 +1,3 @@
-github "algolia/instantsearch-core-swift" ~> 6.5
-github "apple/swift-log" ~> 1.2
+github "algolia/algoliasearch-client-swift" ~> 8.0
+github "algolia/instantsearch-ios-insights" ~> 2.3.1
+github "apple/swift-log" ~> 1.3
diff --git a/Cartfile.resolved b/Cartfile.resolved
deleted file mode 100644
index c17ab64b..00000000
--- a/Cartfile.resolved
+++ /dev/null
@@ -1,3 +0,0 @@
-github "algolia/algoliasearch-client-swift" "7.0.3"
-github "algolia/instantsearch-core-swift" "6.5.0"
-github "algolia/instantsearch-ios-insights" "2.3.2"
diff --git a/Gemfile b/Gemfile
index 4e0aacc0..ed964db6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,7 +4,6 @@ source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
-gem 'cocoapods', '~> 1.8.0'
-gem 'fastlane', '~> 2.141.0'
+gem 'cocoapods', '~> 1.9'
+gem 'fastlane', '~> 2.151'
gem 'xcov'
-gem 'jazzy'
diff --git a/Gemfile.lock b/Gemfile.lock
index cbf30bf9..9193cbb5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,23 +2,39 @@ GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
- activesupport (4.2.11.1)
+ activesupport (4.2.11.3)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
- algoliasearch (1.27.1)
+ algoliasearch (1.27.3)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
+ aws-eventstream (1.1.0)
+ aws-partitions (1.341.0)
+ aws-sdk-core (3.103.0)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.239.0)
+ aws-sigv4 (~> 1.1)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.36.0)
+ aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.74.0)
+ aws-sdk-core (~> 3, >= 3.102.1)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.1)
+ aws-sigv4 (1.2.1)
+ aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
- cocoapods (1.8.4)
+ cocoapods (1.9.3)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0)
- cocoapods-core (= 1.8.4)
+ cocoapods-core (= 1.9.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -33,71 +49,75 @@ GEM
molinillo (~> 0.6.6)
nap (~> 1.0)
ruby-macho (~> 1.4)
- xcodeproj (>= 1.11.1, < 2.0)
- cocoapods-core (1.8.4)
+ xcodeproj (>= 1.14.0, < 2.0)
+ cocoapods-core (1.9.3)
activesupport (>= 4.0.2, < 6)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
+ netrc (~> 0.11)
+ typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
- cocoapods-trunk (1.4.1)
+ cocoapods-trunk (1.5.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
- cocoapods-try (1.1.0)
+ cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
- concurrent-ruby (1.1.5)
- declarative (0.0.10)
+ concurrent-ruby (1.1.6)
+ declarative (0.0.20)
declarative-option (0.1.0)
- digest-crc (0.4.1)
+ digest-crc (0.6.1)
+ rake (~> 13.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
- emoji_regex (1.0.1)
+ emoji_regex (3.0.0)
escape (0.0.4)
- excon (0.72.0)
- faraday (0.17.3)
+ ethon (0.12.0)
+ ffi (>= 1.3.0)
+ excon (0.75.0)
+ faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
- faraday_middleware (0.13.1)
- faraday (>= 0.7.4, < 1.0)
+ faraday_middleware (1.0.0)
+ faraday (~> 1.0)
fastimage (2.1.7)
- fastlane (2.141.0)
+ fastlane (2.151.2)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
- babosa (>= 1.0.2, < 2.0.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
- emoji_regex (>= 0.1, < 2.0)
+ emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
- faraday (~> 0.17)
+ faraday (>= 0.17, < 2.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 0.13.1)
+ faraday_middleware (>= 0.13.1, < 2.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
- google-api-client (>= 0.29.2, < 0.37.0)
+ google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
- jwt (~> 2.1.0)
+ jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
- multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
- public_suffix (~> 2.0.0)
- rubyzip (>= 1.3.0, < 2.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
@@ -109,11 +129,11 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
- ffi (1.12.1)
+ ffi (1.13.1)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
- google-api-client (0.36.4)
+ google-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@@ -124,59 +144,47 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.0)
- faraday (~> 0.11)
- google-cloud-errors (1.0.0)
- google-cloud-storage (1.25.1)
+ google-cloud-env (1.3.2)
+ faraday (>= 0.17.3, < 2.0)
+ google-cloud-errors (1.0.1)
+ google-cloud-storage (1.26.2)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.10.0)
- faraday (~> 0.12)
+ googleauth (0.13.0)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
- signet (~> 0.12)
+ signet (~> 0.14)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
- jazzy (0.13.1)
- cocoapods (~> 1.5)
- mustache (~> 1.1)
- open4
- redcarpet (~> 3.4)
- rouge (>= 2.0.6, < 4.0)
- sassc (~> 2.1)
- sqlite3 (~> 1.3)
- xcinvoke (~> 0.3.0)
- json (2.3.0)
- jwt (2.1.0)
- liferaft (0.0.6)
+ jmespath (1.4.0)
+ json (2.3.1)
+ jwt (2.2.1)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
- minitest (5.14.0)
+ minitest (5.14.1)
molinillo (0.6.6)
- multi_json (1.14.1)
- multi_xml (0.6.0)
+ multi_json (1.15.0)
multipart-post (2.0.0)
- mustache (1.1.1)
nanaimo (0.2.6)
nap (1.1.0)
naturally (2.2.0)
netrc (0.11.0)
- open4 (1.3.4)
- os (1.0.1)
+ os (1.1.0)
plist (3.5.0)
- public_suffix (2.0.5)
- redcarpet (3.5.0)
+ public_suffix (4.0.5)
+ rake (13.0.1)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
@@ -184,45 +192,42 @@ GEM
retriable (3.1.2)
rouge (2.0.7)
ruby-macho (1.4.0)
- rubyzip (1.3.0)
- sassc (2.2.1)
- ffi (~> 1.9)
+ rubyzip (2.3.0)
security (0.1.3)
- signet (0.12.0)
+ signet (0.14.0)
addressable (~> 2.3)
- faraday (~> 0.9)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- simctl (1.6.7)
+ simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
- sqlite3 (1.4.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
tty-cursor (0.7.1)
- tty-screen (0.7.0)
+ tty-screen (0.8.0)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
- tzinfo (1.2.6)
+ typhoeus (1.4.0)
+ ethon (>= 0.9.0)
+ tzinfo (1.2.7)
thread_safe (~> 0.1)
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.6)
- unicode-display_width (1.6.1)
+ unf_ext (0.0.7.7)
+ unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcinvoke (0.3.0)
- liferaft (~> 0.0.6)
- xcodeproj (1.14.0)
+ xcodeproj (1.17.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
- xcov (1.7.2)
+ xcov (1.7.3)
fastlane (>= 2.141.0, < 3.0.0)
multipart-post
slack-notifier
@@ -233,15 +238,14 @@ GEM
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
- xcresult (0.2.0)
+ xcresult (0.2.1)
PLATFORMS
ruby
DEPENDENCIES
- cocoapods (~> 1.8.0)
- fastlane (~> 2.141.0)
- jazzy
+ cocoapods (~> 1.9)
+ fastlane (~> 2.151)
xcov
BUNDLED WITH
diff --git a/InstantSearch.podspec b/InstantSearch.podspec
index 1d19d7ad..8a1f95e9 100644
--- a/InstantSearch.podspec
+++ b/InstantSearch.podspec
@@ -1,34 +1,37 @@
Pod::Spec.new do |s|
- s.name = "InstantSearch"
- s.module_name = 'InstantSearch'
- s.version = "5.2.3"
- s.summary = "A library of widgets and helpers to build instant-search applications on iOS."
- s.homepage = "https://github.com/algolia/instantsearch-ios"
- s.license = { type: 'Apache 2.0', file: 'LICENSE.md' }
- s.author = { "Algolia" => "contact@algolia.com" }
- s.source = { git: "https://github.com/algolia/instantsearch-ios.git", tag: s.version.to_s }
- s.social_media_url = 'https://twitter.com/algolia'
- s.ios.deployment_target = '8.0'
- s.requires_arc = true
- s.default_subspec = "UI"
- s.swift_version = '5.0'
- s.swift_versions = ['4.0', '4.2', '5.0']
+ s.name = 'InstantSearch'
+ s.version = '7.0.0'
+ s.platforms = { :ios => "8.0", :osx => "10.10", :watchos => "3.0", :tvos => "9.0" }
- s.subspec "UI" do |ss|
- ss.source_files = 'Sources/**/*.{swift}'
- ss.dependency 'InstantSearchCore', '~> 6.5'
- end
+ s.license = { type: 'Apache 2.0', file: 'LICENSE.md' }
+ s.summary = 'A library of widgets and helpers to build instant-search applications on iOS.'
+ s.homepage = 'https://github.com/algolia/instantsearch-ios'
+ s.author = { "Algolia" => "contact@algolia.com" }
+ s.source = { :git => 'https://github.com/algolia/instantsearch-ios.git', :tag => s.version }
- s.subspec "Core" do |ss|
- ss.dependency 'InstantSearchCore', '~> 6.5'
- end
-
- s.subspec "Client" do |ss|
- ss.dependency 'InstantSearchClient', '~> 7.0'
- end
-
- # Dependencies
- # ------------
- s.dependency 'InstantSearchCore', '~> 6.5'
- s.dependency 'Logging'
+ s.swift_version = "5.1"
+
+ s.default_subspec = 'UI'
+
+ s.subspec "Core" do |ss|
+ ss.source_files = 'Sources/InstantSearchCore/**/*.{swift}'
+ ss.dependency 'AlgoliaSearchClient', '~> 8.0.0'
+ ss.dependency 'InstantSearchInsights', '~> 2.3'
+ ss.ios.deployment_target = '8.0'
+ ss.osx.deployment_target = '10.10'
+ ss.watchos.deployment_target = '3.0'
+ ss.tvos.deployment_target = '9.0'
+ end
+
+ s.subspec "UI" do |ss|
+ ss.source_files = 'Sources/InstantSearch/**/*.{swift}'
+ ss.dependency 'InstantSearch/Core'
+ ss.ios.deployment_target = '8.0'
+ ss.osx.deployment_target = '10.10'
+ ss.watchos.deployment_target = '3.0'
+ ss.tvos.deployment_target = '9.0'
+ ss.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-DInstantSearchCocoaPods' }
+ end
+
+
end
diff --git a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
deleted file mode 100644
index 57914328..00000000
--- a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "object": {
- "pins": [
- {
- "package": "InstantSearchClient",
- "repositoryURL": "https://github.com/algolia/algoliasearch-client-swift",
- "state": {
- "branch": null,
- "revision": "ef949269c6119a69ed574edb8ccecd67b2e1cf38",
- "version": "7.0.3"
- }
- },
- {
- "package": "InstantSearchCore",
- "repositoryURL": "https://github.com/algolia/instantsearch-core-swift",
- "state": {
- "branch": null,
- "revision": "b5c58a8878c03c5c3ac3ed92c630e7067bc85abb",
- "version": "6.5.1"
- }
- },
- {
- "package": "InstantSearchInsights",
- "repositoryURL": "https://github.com/algolia/instantsearch-ios-insights",
- "state": {
- "branch": null,
- "revision": "1d2f3462b36980c86fdf86529520fc655293b6e4",
- "version": "2.3.2"
- }
- },
- {
- "package": "swift-log",
- "repositoryURL": "https://github.com/apple/swift-log",
- "state": {
- "branch": null,
- "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
- "version": "1.2.0"
- }
- }
- ]
- },
- "version": 1
-}
diff --git a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearch.xcscheme b/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearch.xcscheme
deleted file mode 100644
index f39c0902..00000000
--- a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearch.xcscheme
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTests.xcscheme b/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTests.xcscheme
deleted file mode 100644
index 9d321c44..00000000
--- a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTests.xcscheme
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTestsHost.xcscheme b/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTestsHost.xcscheme
deleted file mode 100644
index bf7d40e4..00000000
--- a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTestsHost.xcscheme
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/InstantSearchTestsHost/AppDelegate.swift b/InstantSearchTestsHost/AppDelegate.swift
deleted file mode 100644
index a57ca745..00000000
--- a/InstantSearchTestsHost/AppDelegate.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// AppDelegate.swift
-// InstantSearchTestsHost
-//
-// Created by Guy Daher on 22/05/2017.
-//
-//
-
-import UIKit
-
-@UIApplicationMain
-class AppDelegate: UIResponder, UIApplicationDelegate {
-
- var window: UIWindow?
-
-
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- // Override point for customization after application launch.
- return true
- }
-
- func applicationWillResignActive(_ application: UIApplication) {
- // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
- // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
- }
-
- func applicationDidEnterBackground(_ application: UIApplication) {
- // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
- // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
- }
-
- func applicationWillEnterForeground(_ application: UIApplication) {
- // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
- }
-
- func applicationDidBecomeActive(_ application: UIApplication) {
- // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
- }
-
- func applicationWillTerminate(_ application: UIApplication) {
- // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
- }
-
-
-}
-
diff --git a/InstantSearchTestsHost/Assets.xcassets/AppIcon.appiconset/Contents.json b/InstantSearchTestsHost/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d8db8d65..00000000
--- a/InstantSearchTestsHost/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "iphone",
- "size" : "20x20",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "20x20",
- "scale" : "3x"
- },
- {
- "idiom" : "iphone",
- "size" : "29x29",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "29x29",
- "scale" : "3x"
- },
- {
- "idiom" : "iphone",
- "size" : "40x40",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "40x40",
- "scale" : "3x"
- },
- {
- "idiom" : "iphone",
- "size" : "60x60",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "60x60",
- "scale" : "3x"
- },
- {
- "idiom" : "ipad",
- "size" : "20x20",
- "scale" : "1x"
- },
- {
- "idiom" : "ipad",
- "size" : "20x20",
- "scale" : "2x"
- },
- {
- "idiom" : "ipad",
- "size" : "29x29",
- "scale" : "1x"
- },
- {
- "idiom" : "ipad",
- "size" : "29x29",
- "scale" : "2x"
- },
- {
- "idiom" : "ipad",
- "size" : "40x40",
- "scale" : "1x"
- },
- {
- "idiom" : "ipad",
- "size" : "40x40",
- "scale" : "2x"
- },
- {
- "idiom" : "ipad",
- "size" : "76x76",
- "scale" : "1x"
- },
- {
- "idiom" : "ipad",
- "size" : "76x76",
- "scale" : "2x"
- },
- {
- "idiom" : "ipad",
- "size" : "83.5x83.5",
- "scale" : "2x"
- },
- {
- "idiom" : "ios-marketing",
- "size" : "1024x1024",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/InstantSearchTestsHost/Base.lproj/LaunchScreen.storyboard b/InstantSearchTestsHost/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index fdf3f97d..00000000
--- a/InstantSearchTestsHost/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/InstantSearchTestsHost/Base.lproj/Main.storyboard b/InstantSearchTestsHost/Base.lproj/Main.storyboard
deleted file mode 100644
index 273375fc..00000000
--- a/InstantSearchTestsHost/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/InstantSearchTestsHost/Info.plist b/InstantSearchTestsHost/Info.plist
deleted file mode 100644
index 1c80ee11..00000000
--- a/InstantSearchTestsHost/Info.plist
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 5.2.3
- CFBundleVersion
- 1
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UIRequiredDeviceCapabilities
-
- armv7
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
-
diff --git a/InstantSearchTestsHost/ViewController.swift b/InstantSearchTestsHost/ViewController.swift
deleted file mode 100644
index a42a40e8..00000000
--- a/InstantSearchTestsHost/ViewController.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// ViewController.swift
-// InstantSearchTestsHost
-//
-// Created by Guy Daher on 22/05/2017.
-//
-//
-
-import UIKit
-
-class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
- // Do any additional setup after loading the view, typically from a nib.
- }
-
- override func didReceiveMemoryWarning() {
- super.didReceiveMemoryWarning()
- // Dispose of any resources that can be recreated.
- }
-
-
-}
-
diff --git a/LICENSE.md b/LICENSE.md
index 0fa1803a..8b1b30c6 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,203 +1,201 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2019 Algolia
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2019 Algolia
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/Package.resolved b/Package.resolved
deleted file mode 100644
index eece6c2b..00000000
--- a/Package.resolved
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "object": {
- "pins": [
- {
- "package": "InstantSearchClient",
- "repositoryURL": "https://github.com/algolia/algoliasearch-client-swift",
- "state": {
- "branch": null,
- "revision": "ef949269c6119a69ed574edb8ccecd67b2e1cf38",
- "version": "7.0.3"
- }
- },
- {
- "package": "InstantSearchCore",
- "repositoryURL": "https://github.com/algolia/instantsearch-core-swift",
- "state": {
- "branch": null,
- "revision": "b5c58a8878c03c5c3ac3ed92c630e7067bc85abb",
- "version": "6.5.1"
- }
- },
- {
- "package": "InstantSearchInsights",
- "repositoryURL": "https://github.com/algolia/instantsearch-ios-insights",
- "state": {
- "branch": null,
- "revision": "1d2f3462b36980c86fdf86529520fc655293b6e4",
- "version": "2.3.2"
- }
- },
- {
- "package": "swift-log",
- "repositoryURL": "https://github.com/apple/swift-log.git",
- "state": {
- "branch": null,
- "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
- "version": "1.2.0"
- }
- }
- ]
- },
- "version": 1
-}
diff --git a/Package.swift b/Package.swift
index 842eabae..d7a7ad32 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.1
+// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,29 +6,35 @@ import PackageDescription
let package = Package(
name: "InstantSearch",
platforms: [
- .iOS(SupportedPlatform.IOSVersion.v8),
+ .iOS(.v8),
+ .macOS(.v10_10),
+ .watchOS(.v2),
+ .tvOS(.v9)
],
products: [
- // Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "InstantSearch",
- targets: ["InstantSearch"]),
+ targets: ["InstantSearch", "InstantSearchCore"]),
+ .library(
+ name: "InstantSearchCore",
+ targets: ["InstantSearchCore"])
],
dependencies: [
- // Dependencies declare other packages that this package depends on.
- .package(url:"https://github.com/algolia/instantsearch-core-swift", from: "6.5.1"),
- .package(url:"https://github.com/apple/swift-log.git", from: "1.2.0"),
+ .package(name: "AlgoliaSearchClient", url:"https://github.com/algolia/algoliasearch-client-swift", from: "8.0.0"),
+ .package(name: "InstantSearchInsights", url:"https://github.com/algolia/instantsearch-ios-insights", from: "2.3.2")
],
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: "InstantSearchCore",
+ dependencies: ["AlgoliaSearchClient", "InstantSearchInsights"]),
.target(
name: "InstantSearch",
- dependencies: ["InstantSearchCore", "Logging"],
- path: "./Sources"),
+ dependencies: ["InstantSearchCore"]),
+ .testTarget(
+ name: "InstantSearchCoreTests",
+ dependencies: ["InstantSearchCore", "AlgoliaSearchClient", "InstantSearchInsights"]),
.testTarget(
name: "InstantSearchTests",
- dependencies: ["InstantSearch", "InstantSearchCore", "Logging"],
- path: "./Tests")
+ dependencies: ["InstantSearch"])
]
)
diff --git a/Readme.md b/Readme.md
index 43560515..7c3e9543 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,12 +1,11 @@
-![InstantSearch iOS](./docgen/assets/img/instantsearch-banner.png)
-
-
-
-
-
-
-
-
+![InstantSearch iOS](./Resources/instantsearch-banner.png)
+
+[![Pod Version](http://img.shields.io/cocoapods/v/InstantSearch.svg?style=flat)](https://github.com/algolia/instantsearch-ios/)
+[![Pod Platform](http://img.shields.io/cocoapods/p/InstantSearch.svg?style=flat)](https://github.com/algolia/instantsearch-ios/)
+[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg)](https://github.com/algolia/instantsearch-ios/)
+[![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/)
+[![Mac Catalyst compatible](https://img.shields.io/badge/Catalyst-compatible-brightgreen.svg)](https://developer.apple.com/documentation/xcode/creating_a_mac_version_of_your_ipad_app/)
+[![Licence](http://img.shields.io/cocoapods/l/InstantSearch.svg?style=flat)](https://opensource.org/licenses/Apache-2.0)
By [Algolia](http://algolia.com).
@@ -14,42 +13,47 @@ InstantSearch family: **InstantSearch iOS** | [InstantSearch Android][instantsea
**InstantSearch iOS** is a framework providing components and helpers to help you build the best instant-search experience on iOS with Algolia. It is built on top of Algolia's [Swift API Client](https://github.com/algolia/algoliasearch-client-swift) library to provide you a high-level solution to quickly build various search interfaces.
-
-
## Demo
You can see InstantSearch iOS in action in our [Examples repository][examples-url], in which we published search experiences built with InstantSearch and written in Swift:
-
+
[examples-url]: https://github.com/algolia/instantsearch-swift-examples
## Installation
-### CocoaPods
+### Swift Package Manager
-[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects.
+The Swift Package Manager is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
+Since the release of Swift 5 and Xcode 11, SPM is compatible with the iOS, macOS and tvOS build systems for creating apps.
-To install InstantSearch, simply add the following line to your Podfile:
+To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [InstantSearch repo's URL](https://github.com/algolia/instantsearch-ios).
+If you consider to use only the business logic modules of InstantSearch and don't need the set of provided UIKit controllers in your project, select only 'InstantSearchCore' in the provided list of products.
-#### Swift 4.2+
+If you're a framework author and use InstantSearch as a dependency, update your `Package.swift` file:
-```ruby
-pod 'InstantSearch', '~> 5.0'
-# pod 'InstantSearch/UI' for access to everything
-# pod 'InstantSearch/Core' for access to everything except the UI controllers
-# pod 'InstantSearch/Client' for access only to the API Client
+```swift
+let package = Package(
+ // 7.0.0 ..< 8.0.0
+ dependencies: [
+ .package(url: "https://github.com/algolia/instantsearch-ios", from: "7.0.0")
+ ],
+ // ...
+)
```
-#### Swift 4.1
+### CocoaPods
+
+[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects.
+
+To install InstantSearch, simply add the following line to your Podfile:
```ruby
-pod 'InstantSearch', '~> 2.0'
-# pod 'InstantSearch/Widgets' for access to everything
-# pod 'InstantSearch/Core' for access to everything except the UI widgets
-# pod 'InstantSearch/Client' for access only to the API Client
+pod 'InstantSearch', '~> 7.0.0'
+# pod 'InstantSearch/Core' for access to everything except the UI controllers
```
Then, run the following command:
@@ -62,36 +66,22 @@ $ pod update
[Carthage](https://github.com/Carthage/Carthage) is a simple, decentralized dependency manager for Cocoa.
-To install InstantSearch, simply add the following line to your Cartfile:
-
-#### Swift 4.2+
-
-```ruby
-github "algolia/instantsearch-ios" ~> 5.0 # for access to everything
-# github "algolia/instantsearch-core-swift" ~> 6.0 # for access to everything except the UI widgets
-# github "algolia/algoliasearch-client-swift" ~> 7.0 # for access only to the API Client
-```
-
-#### Swift 4.1
-
+- To install InstantSearch, simply add the following line to your Cartfile:
```ruby
-github "algolia/instantsearch-ios" ~> 2.0 # for access to everything
-# github "algolia/instantsearch-core-swift" ~> 3.0 # for access to everything except the UI widgets
-# github "algolia/algoliasearch-client-swift" ~> 5.0 # for access only to the API Client
+github "algolia/instantsearch-ios" ~> 7.0.0
```
-### SwiftPM
+- Launch the following commands from the project directory
+ ```shell
+ carthage update
+ ./Carthage/Checkouts/instant-search-ios/carthage-prebuild
+ carthage build
+ ```
-The API client is the only library of the framework available on SwiftPM.
-
-#### Swift 4.2+
-
-To install the API Client, add `.package(url:"https://github.com/algolia/algoliasearch-client-swift", from: "6.0.0")` to your package dependencies array in Package.swift, then add `AlgoliaSearch` to your target dependencies.
-
-
-#### Swift 4.1
-
-To install the API Client, add `.package(url:"https://github.com/algolia/algoliasearch-client-swift", from: "5.0.0")` to your package dependencies array in Package.swift, then add `AlgoliaSearch` to your target dependencies.
+ > NOTE: At this time, Carthage does not provide a way to build only specific repository subcomponents (or equivalent of CocoaPods's subspecs). All components and their dependencies will be built with the above command. However, you don't need to copy frameworks you aren't using into your project. For instance, if you aren't using UI components from `InstantSearch`, feel free to delete that framework from the Carthage Build directory after `carthage update` completes keeping only `InstantSearchCore`.
+
+ If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
+
## Documentation
@@ -174,6 +164,8 @@ Run your app and you will the most basic search experience: a `UISearchBar` with
To get a more meaningful search experience, please follow our [Getting Started Guide](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/).
+If you only require business logic modules in your project and use `InstantSearchCore` framework, add `import InstantSearchCore` to your source files.
+
## Getting Help
- **Need help**? Ask a question to the [Algolia Community](https://discourse.algolia.com/) or on [Stack Overflow](http://stackoverflow.com/questions/tagged/algolia).
diff --git a/Resources/instant-results.gif b/Resources/instant-results.gif
new file mode 100644
index 00000000..24910162
Binary files /dev/null and b/Resources/instant-results.gif differ
diff --git a/docgen/assets/img/instantsearch-banner.png b/Resources/instantsearch-banner.png
similarity index 100%
rename from docgen/assets/img/instantsearch-banner.png
rename to Resources/instantsearch-banner.png
diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift
deleted file mode 100644
index 9b1fdb4e..00000000
--- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// CollectionViewMultiIndexHitsController.swift
-// InstantSearchCore
-//
-// Created by Vladislav Fitc on 25/03/2019.
-// Copyright © 2019 Algolia. All rights reserved.
-//
-
-import Foundation
-import InstantSearchCore
-import UIKit
-
-public class MultiIndexHitsCollectionController: NSObject, MultiIndexHitsController, HitsCollectionViewContainer {
-
- public let collectionView: UICollectionView
-
- public var hitsCollectionView: UICollectionView {
- return collectionView
- }
-
- @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
- public weak var hitsSource: MultiIndexHitsSource? {
- didSet {
- dataSource?.hitsSource = hitsSource
- delegate?.hitsSource = hitsSource
- }
- }
-
- @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
- public var dataSource: MultiIndexHitsCollectionViewDataSource? {
- didSet {
- dataSource?.hitsSource = hitsSource
- collectionView.dataSource = dataSource
- }
- }
-
- public var delegate: MultiIndexHitsCollectionViewDelegate? {
- didSet {
- delegate?.hitsSource = hitsSource
- collectionView.delegate = delegate
- }
- }
-
- public init(collectionView: UICollectionView) {
- self.collectionView = collectionView
- }
-
-}
diff --git a/Sources/FatalErrorUtil.swift b/Sources/FatalErrorUtil.swift
deleted file mode 100644
index 9038fab3..00000000
--- a/Sources/FatalErrorUtil.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// FatalErrorUtil.swift
-// InstantSearch
-//
-// Created by Vladislav Fitc on 04/09/2019.
-//
-
-import Foundation
-
-// overrides Swift global `fatalError`
-public func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never {
- FatalErrorUtil.fatalErrorClosure(message(), file, line)
- unreachable()
-}
-
-/// This is a `noreturn` function that pauses forever
-public func unreachable() -> Never {
- repeat {
- RunLoop.current.run()
- } while (true)
-}
-
-/// Utility functions that can replace and restore the `fatalError` global function.
-public struct FatalErrorUtil {
-
- // Called by the custom implementation of `fatalError`.
- static var fatalErrorClosure: (String, StaticString, UInt) -> Never = defaultFatalErrorClosure
-
- // backup of the original Swift `fatalError`
- private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) }
-
- /// Replace the `fatalError` global function with something else.
- public static func replaceFatalError(closure: @escaping (String, StaticString, UInt) -> Never) {
- fatalErrorClosure = closure
- }
-
- /// Restore the `fatalError` global function back to the original Swift implementation
- public static func restoreFatalError() {
- fatalErrorClosure = defaultFatalErrorClosure
- }
-}
diff --git a/Sources/Info.plist b/Sources/Info.plist
deleted file mode 100644
index 5313a0db..00000000
--- a/Sources/Info.plist
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 5.1.0
- CFBundleSignature
- ????
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- NSPrincipalClass
-
-
-
diff --git a/Sources/InstantSearch-Bridging-Header.h b/Sources/InstantSearch-Bridging-Header.h
deleted file mode 100644
index ce3b8314..00000000
--- a/Sources/InstantSearch-Bridging-Header.h
+++ /dev/null
@@ -1,13 +0,0 @@
-//
-// InstantSearch-Bridging-Header.h
-// InstantSearch
-//
-// Created by Guy Daher on 12/05/2017.
-//
-//
-
-#ifndef InstantSearch_Bridging_Header_h
-#define InstantSearch_Bridging_Header_h
-
-
-#endif /* InstantSearch_Bridging_Header_h */
diff --git a/Sources/Controller/ClearRefinements/ClearRefinementsButtonController.swift b/Sources/InstantSearch/ClearRefinements/ClearRefinementsButtonController.swift
similarity index 76%
rename from Sources/Controller/ClearRefinements/ClearRefinementsButtonController.swift
rename to Sources/InstantSearch/ClearRefinements/ClearRefinementsButtonController.swift
index 26f1fe84..95e071fc 100644
--- a/Sources/Controller/ClearRefinements/ClearRefinementsButtonController.swift
+++ b/Sources/InstantSearch/ClearRefinements/ClearRefinementsButtonController.swift
@@ -5,23 +5,26 @@
// Created by Vladislav Fitc on 24/05/2019.
//
-import Foundation
-import InstantSearchCore
+#if !InstantSearchCocoaPods
+@_exported import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public class FilterClearButtonController: FilterClearController {
-
+
public let button: UIButton
-
+
public var onClick: (() -> Void)?
-
+
public init(button: UIButton) {
self.button = button
button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
}
-
+
@objc private func didTapButton() {
onClick?()
}
-
+
}
+#endif
diff --git a/Sources/Controller/CurrentFilters/CurrentFiltersSearchTextFieldController.swift b/Sources/InstantSearch/CurrentFilters/CurrentFiltersSearchTextFieldController.swift
similarity index 92%
rename from Sources/Controller/CurrentFilters/CurrentFiltersSearchTextFieldController.swift
rename to Sources/InstantSearch/CurrentFilters/CurrentFiltersSearchTextFieldController.swift
index 7095c0b3..0851dc2c 100644
--- a/Sources/Controller/CurrentFilters/CurrentFiltersSearchTextFieldController.swift
+++ b/Sources/InstantSearch/CurrentFilters/CurrentFiltersSearchTextFieldController.swift
@@ -5,29 +5,32 @@
// Created by Vladislav Fitc on 29/01/2020.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(macOS))
import UIKit
@available(iOS 13.0, *)
public class SearchTextFieldCurrentFiltersController: CurrentFiltersController {
-
+
public var items: [FilterAndID] = []
public var onRemoveItem: ((FilterAndID) -> Void)?
public let searchTextField: UISearchTextField
-
+
public init(searchTextField: UISearchTextField) {
self.searchTextField = searchTextField
searchTextField.addTarget(self, action: #selector(didChange), for: .editingChanged)
}
-
+
public convenience init(searchBar: UISearchBar) {
self.init(searchTextField: searchBar.searchTextField)
}
-
+
public func setItems(_ items: [FilterAndID]) {
self.items = Array(items)
}
-
+
public func reload() {
let tokens = items
.enumerated()
@@ -38,7 +41,7 @@ public class SearchTextFieldCurrentFiltersController: CurrentFiltersController {
}
searchTextField.tokens = tokens
}
-
+
@objc private func didChange(_ textField: UITextField) {
let previousFiltersIndices = Set(0.. Void)?
-
+
public let tableView: UITableView
-
+
public var selectableItems: [SelectableItem] = []
public var facetPresenter: FacetPresenter?
-
+
let cellID: String
-
+
public init(tableView: UITableView, cellID: String = "FacetList") {
self.tableView = tableView
self.cellID = cellID
@@ -28,42 +30,43 @@ open class FacetListTableController: NSObject, FacetListController {
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
}
-
+
// MARK: - RefinementFacetsViewController
-
+
public func setSelectableItems(selectableItems: [SelectableItem]) {
self.selectableItems = selectableItems
}
-
+
public func reload() {
tableView.reloadData()
}
-
+
}
extension FacetListTableController: UITableViewDataSource {
-
+
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return selectableItems.count
}
-
+
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
let selectableRefinement = selectableItems[indexPath.row]
let facetPresenter = self.facetPresenter ?? DefaultPresenter.Facet.present
cell.textLabel?.text = facetPresenter(selectableRefinement.item)
cell.accessoryType = selectableRefinement.isSelected ? .checkmark : .none
-
+
return cell
}
-
+
}
extension FacetListTableController: UITableViewDelegate {
-
+
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectableItem = selectableItems[indexPath.row]
self.onClick?(selectableItem.item)
}
-
+
}
+#endif
diff --git a/Sources/Controller/FilterList/FilterListTableController.swift b/Sources/InstantSearch/FilterList/FilterListTableController.swift
similarity index 94%
rename from Sources/Controller/FilterList/FilterListTableController.swift
rename to Sources/InstantSearch/FilterList/FilterListTableController.swift
index ff17bdc7..43ace190 100644
--- a/Sources/Controller/FilterList/FilterListTableController.swift
+++ b/Sources/InstantSearch/FilterList/FilterListTableController.swift
@@ -5,23 +5,25 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
open class FilterListTableController: NSObject, FilterListController, UITableViewDataSource, UITableViewDelegate {
-
+
public typealias Item = F
-
+
open var onClick: ((F) -> Void)?
-
+
public let tableView: UITableView
-
+
public var selectableItems: [SelectableItem] = []
public var filterPresenter: FilterPresenter?
-
+
let cellID: String
-
+
public init(tableView: UITableView, cellID: String = "FilterListFacet") {
self.tableView = tableView
self.cellID = cellID
@@ -30,38 +32,39 @@ open class FilterListTableController: NSObject, FilterListControl
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
}
-
+
// MARK: - FilterListController
-
+
open func setSelectableItems(selectableItems: [SelectableItem]) {
self.selectableItems = selectableItems
}
-
+
open func reload() {
tableView.reloadData()
}
-
+
// MARK: - UITableViewDataSource
-
+
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return selectableItems.count
}
-
+
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
let filter = selectableItems[indexPath.row]
let filterPresenter = self.filterPresenter ?? DefaultPresenter.Filter.present
cell.textLabel?.text = filterPresenter(Filter(filter.item))
cell.accessoryType = filter.isSelected ? .checkmark : .none
-
+
return cell
}
-
+
// MARK: - UITableViewDelegate
-
+
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectableItem = selectableItems[indexPath.row]
onClick?(selectableItem.item)
}
-
+
}
+#endif
diff --git a/Sources/Helpers/UICollectionView+Convenience.swift b/Sources/InstantSearch/Helpers/UICollectionView+Convenience.swift
similarity index 85%
rename from Sources/Helpers/UICollectionView+Convenience.swift
rename to Sources/InstantSearch/Helpers/UICollectionView+Convenience.swift
index cb0a1ef8..580cc324 100644
--- a/Sources/Helpers/UICollectionView+Convenience.swift
+++ b/Sources/InstantSearch/Helpers/UICollectionView+Convenience.swift
@@ -6,16 +6,19 @@
//
import Foundation
+
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public extension UICollectionView {
-
+
func scrollToFirstNonEmptySection() {
(0.. 0 })
.flatMap { IndexPath(item: 0, section: $0) }
.flatMap { scrollToItem(at: $0, at: .top, animated: false) }
-
+
}
-
+
}
+#endif
diff --git a/Sources/Helpers/UITableView+Convenience.swift b/Sources/InstantSearch/Helpers/UITableView+Convenience.swift
similarity index 85%
rename from Sources/Helpers/UITableView+Convenience.swift
rename to Sources/InstantSearch/Helpers/UITableView+Convenience.swift
index 6c5435b7..2ad1120e 100644
--- a/Sources/Helpers/UITableView+Convenience.swift
+++ b/Sources/InstantSearch/Helpers/UITableView+Convenience.swift
@@ -6,15 +6,18 @@
//
import Foundation
+
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public extension UITableView {
-
+
func scrollToFirstNonEmptySection() {
(0.. 0 })
.flatMap { IndexPath(row: 0, section: $0) }
.flatMap { scrollToRow(at: $0, at: .top, animated: false) }
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hierarchical/HierarchicalTableViewController.swift b/Sources/InstantSearch/Hierarchical/HierarchicalTableViewController.swift
similarity index 94%
rename from Sources/Controller/Hierarchical/HierarchicalTableViewController.swift
rename to Sources/InstantSearch/Hierarchical/HierarchicalTableViewController.swift
index 8f399d3b..8c259991 100644
--- a/Sources/Controller/Hierarchical/HierarchicalTableViewController.swift
+++ b/Sources/InstantSearch/Hierarchical/HierarchicalTableViewController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
open class HierarchicalTableViewController: NSObject, HierarchicalController {
@@ -63,3 +65,4 @@ extension HierarchicalTableViewController: UITableViewDelegate {
onClick?(item.facet.value)
}
}
+#endif
diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionController.swift
similarity index 86%
rename from Sources/Controller/Hits/CollectionView/HitsCollectionController.swift
rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionController.swift
index 33b8d1ec..f55095c9 100644
--- a/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift
+++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public typealias HitViewConfigurator = (HitsView, Hit, IndexPath) -> SingleHitView
@@ -16,43 +18,42 @@ public typealias HitClickHandler = (HitsView, Hit, IndexPath) ->
public typealias CollectionViewCellConfigurator- = HitViewConfigurator
public typealias CollectionViewClickHandler- = HitClickHandler
+@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol")
public class HitsCollectionController: NSObject, HitsController, HitsCollectionViewContainer {
-
+
public let collectionView: UICollectionView
-
+
public var hitsCollectionView: UICollectionView {
return collectionView
}
-
+
public weak var hitsSource: Source?
- @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
public var dataSource: HitsCollectionViewDataSource? {
didSet {
dataSource?.hitsSource = hitsSource
collectionView.dataSource = dataSource
}
}
-
- @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
+
public var delegate: HitsCollectionViewDelegate? {
didSet {
delegate?.hitsSource = hitsSource
collectionView.delegate = delegate
}
}
-
+
public init(collectionView: UICollectionView) {
self.collectionView = collectionView
}
-
+
// These functions are implemented in the protocol extension, but should be there till
// compiler bug is fixed
public func reload() {
hitsCollectionView.reloadData()
}
-
+
public func scrollToTop() {
guard hitsCollectionView.numberOfItems(inSection: 0) != 0 else { return }
let indexPath = IndexPath(item: 0, section: 0)
@@ -60,3 +61,4 @@ public class HitsCollectionController: NSObject, HitsControl
}
}
+#endif
diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewContainer.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewContainer.swift
similarity index 87%
rename from Sources/Controller/Hits/CollectionView/HitsCollectionViewContainer.swift
rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewContainer.swift
index 33916c31..81d042ad 100644
--- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewContainer.swift
+++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewContainer.swift
@@ -5,39 +5,43 @@
// Created by Vladislav Fitc on 02/09/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public protocol HitsCollectionViewContainer {
-
+
var hitsCollectionView: UICollectionView { get }
-
+
}
public extension HitsController where Self: HitsCollectionViewContainer {
-
+
func reload() {
hitsCollectionView.reloadData()
}
-
+
func scrollToTop() {
guard hitsCollectionView.numberOfItems(inSection: 0) != 0 else { return }
let indexPath = IndexPath(row: 0, section: 0)
self.hitsCollectionView.scrollToItem(at: indexPath, at: .top, animated: false)
}
-
+
}
public extension MultiIndexHitsController where Self: HitsCollectionViewContainer {
-
+
func reload() {
hitsCollectionView.reloadData()
}
-
+
func scrollToTop() {
guard hitsCollectionView.numberOfItems(inSection: 0) != 0 else { return }
let indexPath = IndexPath(item: 0, section: 0)
hitsCollectionView.scrollToItem(at: indexPath, at: .top, animated: false)
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDataSource.swift
similarity index 83%
rename from Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift
rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDataSource.swift
index f73efbc3..0f399e86 100644
--- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift
+++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDataSource.swift
@@ -5,45 +5,49 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
+@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol")
open class HitsCollectionViewDataSource: NSObject, UICollectionViewDataSource {
-
+
public var cellConfigurator: CollectionViewCellConfigurator
public var templateCellProvider: () -> UICollectionViewCell
public weak var hitsSource: DataSource?
-
+
public init(cellConfigurator: @escaping CollectionViewCellConfigurator) {
self.cellConfigurator = cellConfigurator
self.templateCellProvider = { return .init() }
}
-
+
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
-
+
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
return 0
}
return hitsSource.numberOfHits()
-
+
}
-
+
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
-
+
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
return templateCellProvider()
}
-
+
guard let hit = hitsSource.hit(atIndex: indexPath.row) else {
return templateCellProvider()
}
-
+
return cellConfigurator(collectionView, hit, indexPath)
-
+
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDelegate.swift
similarity index 76%
rename from Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift
rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDelegate.swift
index a76e5c6d..cf183f69 100644
--- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift
+++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDelegate.swift
@@ -5,21 +5,24 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
+@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol")
open class HitsCollectionViewDelegate: NSObject, UICollectionViewDelegate {
-
+
public var clickHandler: CollectionViewClickHandler
public weak var hitsSource: DataSource?
-
+
public init(clickHandler: @escaping CollectionViewClickHandler) {
self.clickHandler = clickHandler
}
-
+
open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
-
+
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
return
@@ -29,7 +32,8 @@ open class HitsCollectionViewDelegate: NSObject, UIColle
return
}
clickHandler(collectionView, hit, indexPath)
-
+
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift b/Sources/InstantSearch/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift
similarity index 82%
rename from Sources/Controller/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift
rename to Sources/InstantSearch/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift
index 379af40c..a51e6c19 100644
--- a/Sources/Controller/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift
+++ b/Sources/InstantSearch/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift
@@ -6,12 +6,15 @@
//
import Foundation
+
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
extension UICollectionViewController: HitsCollectionViewContainer {
-
+
public var hitsCollectionView: UICollectionView {
return collectionView
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/TableView/HitsTableController.swift b/Sources/InstantSearch/Hits/TableView/HitsTableController.swift
similarity index 85%
rename from Sources/Controller/Hits/TableView/HitsTableController.swift
rename to Sources/InstantSearch/Hits/TableView/HitsTableController.swift
index 14be48a1..0701af5e 100644
--- a/Sources/Controller/Hits/TableView/HitsTableController.swift
+++ b/Sources/InstantSearch/Hits/TableView/HitsTableController.swift
@@ -6,59 +6,61 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public typealias TableViewCellConfigurator- = HitViewConfigurator
public typealias TableViewClickHandler- = HitClickHandler
+@available(*, unavailable, message: "Use your own UITableViewController conforming to HitsController protocol")
public class HitsTableController: NSObject, HitsController, HitsTableViewContainer {
-
+
public var hitsTableView: UITableView {
return tableView
}
-
+
public let tableView: UITableView
-
+
public weak var hitsSource: Source? {
didSet {
dataSource?.hitsSource = hitsSource
delegate?.hitsSource = hitsSource
}
}
-
- @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
+
public var dataSource: HitsTableViewDataSource? {
didSet {
dataSource?.hitsSource = hitsSource
tableView.dataSource = dataSource
}
}
-
- @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
+
public var delegate: HitsTableViewDelegate? {
didSet {
delegate?.hitsSource = hitsSource
tableView.delegate = delegate
}
}
-
+
public init(tableView: UITableView) {
self.tableView = tableView
}
-
+
// These functions are implemented in the protocol extension, but should be there till
// compiler bug is fixed
public func reload() {
hitsTableView.reloadData()
}
-
+
public func scrollToTop() {
guard hitsTableView.numberOfRows(inSection: 0) != 0 else { return }
let indexPath = IndexPath(row: 0, section: 0)
self.hitsTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/TableView/HitsTableViewContainer.swift b/Sources/InstantSearch/Hits/TableView/HitsTableViewContainer.swift
similarity index 87%
rename from Sources/Controller/Hits/TableView/HitsTableViewContainer.swift
rename to Sources/InstantSearch/Hits/TableView/HitsTableViewContainer.swift
index addd86ab..17300944 100644
--- a/Sources/Controller/Hits/TableView/HitsTableViewContainer.swift
+++ b/Sources/InstantSearch/Hits/TableView/HitsTableViewContainer.swift
@@ -5,39 +5,43 @@
// Created by Vladislav Fitc on 02/09/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public protocol HitsTableViewContainer {
-
+
var hitsTableView: UITableView { get }
-
+
}
public extension HitsController where Self: HitsTableViewContainer {
-
+
func reload() {
hitsTableView.reloadData()
}
-
+
func scrollToTop() {
guard hitsTableView.numberOfRows(inSection: 0) != 0 else { return }
let indexPath = IndexPath(row: 0, section: 0)
self.hitsTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
-
+
}
public extension MultiIndexHitsController where Self: HitsTableViewContainer {
-
+
func reload() {
hitsTableView.reloadData()
}
-
+
func scrollToTop() {
guard hitsTableView.numberOfRows(inSection: 0) != 0 else { return }
let indexPath = IndexPath(item: 0, section: 0)
hitsTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift b/Sources/InstantSearch/Hits/TableView/HitsTableViewDataSource.swift
similarity index 82%
rename from Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift
rename to Sources/InstantSearch/Hits/TableView/HitsTableViewDataSource.swift
index a5fb23eb..bf230b1f 100644
--- a/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift
+++ b/Sources/InstantSearch/Hits/TableView/HitsTableViewDataSource.swift
@@ -5,45 +5,49 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
+@available(*, unavailable, message: "Use your own UITableViewController conforming to HitsController protocol")
open class HitsTableViewDataSource: NSObject, UITableViewDataSource {
-
+
public var cellConfigurator: TableViewCellConfigurator
public var templateCellProvider: () -> UITableViewCell
public weak var hitsSource: DataSource?
-
+
public init(cellConfigurator: @escaping TableViewCellConfigurator) {
self.cellConfigurator = cellConfigurator
self.templateCellProvider = { return .init() }
}
-
+
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-
+
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
return 0
}
-
+
return hitsSource.numberOfHits()
-
+
}
-
+
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
+
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
return .init()
}
-
+
guard let hit = hitsSource.hit(atIndex: indexPath.row) else {
return templateCellProvider()
}
-
+
return cellConfigurator(tableView, hit, indexPath)
-
+
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift b/Sources/InstantSearch/Hits/TableView/HitsTableViewDelegate.swift
similarity index 75%
rename from Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift
rename to Sources/InstantSearch/Hits/TableView/HitsTableViewDelegate.swift
index b732f2a6..6f73ddb6 100644
--- a/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift
+++ b/Sources/InstantSearch/Hits/TableView/HitsTableViewDelegate.swift
@@ -5,21 +5,24 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
+@available(*, unavailable, message: "Use your own UITableViewController conforming to HitsController protocol")
open class HitsTableViewDelegate: NSObject, UITableViewDelegate {
-
+
public var clickHandler: TableViewClickHandler
public weak var hitsSource: DataSource?
-
+
public init(clickHandler: @escaping TableViewClickHandler) {
self.clickHandler = clickHandler
}
-
+
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-
+
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
return
@@ -29,7 +32,8 @@ open class HitsTableViewDelegate: NSObject, UITableViewD
return
}
clickHandler(tableView, hit, indexPath)
-
+
}
-
+
}
+#endif
diff --git a/Sources/Controller/Hits/TableView/UITableViewController+HitsTableViewContainer.swift b/Sources/InstantSearch/Hits/TableView/UITableViewController+HitsTableViewContainer.swift
similarity index 80%
rename from Sources/Controller/Hits/TableView/UITableViewController+HitsTableViewContainer.swift
rename to Sources/InstantSearch/Hits/TableView/UITableViewController+HitsTableViewContainer.swift
index 150b0680..c756543b 100644
--- a/Sources/Controller/Hits/TableView/UITableViewController+HitsTableViewContainer.swift
+++ b/Sources/InstantSearch/Hits/TableView/UITableViewController+HitsTableViewContainer.swift
@@ -6,12 +6,15 @@
//
import Foundation
+
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
extension UITableViewController: HitsTableViewContainer {
-
+
public var hitsTableView: UITableView {
return tableView
}
-
+
}
+#endif
diff --git a/Sources/Controller/LoadingIndicator/ActivityIndicatorController.swift b/Sources/InstantSearch/LoadingIndicator/ActivityIndicatorController.swift
similarity index 86%
rename from Sources/Controller/LoadingIndicator/ActivityIndicatorController.swift
rename to Sources/InstantSearch/LoadingIndicator/ActivityIndicatorController.swift
index 523f50c4..c76e63f8 100644
--- a/Sources/Controller/LoadingIndicator/ActivityIndicatorController.swift
+++ b/Sources/InstantSearch/LoadingIndicator/ActivityIndicatorController.swift
@@ -5,14 +5,16 @@
// Created by Vladislav Fitc on 24/05/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public class ActivityIndicatorController: LoadingController {
let activityIndicator: UIActivityIndicatorView
-
+
public init (activityIndicator: UIActivityIndicatorView) {
self.activityIndicator = activityIndicator
}
@@ -20,13 +22,14 @@ public class ActivityIndicatorController: LoadingController {
public func startLoading() {
activityIndicator.startAnimating()
}
-
+
public func stopLoading() {
activityIndicator.stopAnimating()
}
-
+
public func setItem(_ item: Bool) {
item ? activityIndicator.startAnimating() : activityIndicator.stopAnimating()
}
-
+
}
+#endif
diff --git a/Sources/Controller/Logging.swift b/Sources/InstantSearch/Log.swift
similarity index 60%
rename from Sources/Controller/Logging.swift
rename to Sources/InstantSearch/Log.swift
index 07737763..04a7b6f1 100644
--- a/Sources/Controller/Logging.swift
+++ b/Sources/InstantSearch/Log.swift
@@ -5,96 +5,67 @@
// Created by Vladislav Fitc on 31/01/2020.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
import Logging
-typealias SwiftLog = Logging.Logger
-
struct Logger {
-
- static var loggingService: Loggable = SwiftLog(label: "com.algolia.InstantSearch")
-
+
+ static var loggingService: Loggable = {
+ var swiftLog = Logging.Logger(label: "com.algolia.InstantSearch")
+ print("InstantSearch: Default minimal log severity level is info. Change InstantSearch.Logger.minLogServerityLevel value if you want to change it.")
+ swiftLog.logLevel = .info
+ return swiftLog
+ }()
+
private init() {}
-
+
static func trace(_ message: String) {
loggingService.log(level: .trace, message: message)
}
-
+
static func debug(_ message: String) {
loggingService.log(level: .debug, message: message)
}
-
+
static func info(_ message: String) {
loggingService.log(level: .info, message: message)
}
-
+
static func notice(_ message: String) {
loggingService.log(level: .notice, message: message)
}
-
+
static func warning(_ message: String) {
loggingService.log(level: .warning, message: message)
}
-
+
static func error(_ message: String) {
loggingService.log(level: .error, message: message)
}
-
+
static func critical(_ message: String) {
loggingService.log(level: .critical, message: message)
}
-
-}
-enum LogLevel {
- case trace, debug, info, notice, warning, error, critical
}
+#endif
extension Logger {
-
+
static func missingHitsSourceWarning() {
warning("Missing hits source")
}
-
+
static func missingCellConfiguratorWarning(forSection section: Int) {
warning("No cell configurator found for section \(section)")
}
-
+
static func missingClickHandlerWarning(forSection section: Int) {
warning("No click handler found for section \(section)")
}
-
+
static func error(_ error: Error) {
self.error("\(error)")
}
}
-
-extension LogLevel {
-
- var swiftLogLevel: SwiftLog.Level {
- switch self {
- case .trace: return .trace
- case .debug: return .debug
- case .info: return .info
- case .notice: return .notice
- case .warning: return .warning
- case .error: return .error
- case .critical: return .critical
- }
- }
-
-}
-
-protocol Loggable {
-
- func log(level: LogLevel, message: String)
-
-}
-
-extension SwiftLog: Loggable {
-
- func log(level: LogLevel, message: String) {
- self.log(level: level.swiftLogLevel, SwiftLog.Message(stringLiteral: message), metadata: .none)
- }
-
-}
diff --git a/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift
new file mode 100644
index 00000000..a27805a0
--- /dev/null
+++ b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift
@@ -0,0 +1,36 @@
+//
+// CollectionViewMultiIndexHitsController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 25/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+// swiftlint:disable weak_delegate
+
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
+import UIKit
+
+@available(*, unavailable, message: "Use your own UICollectionViewController conforming to MultiIndexHitsController protocol")
+public class MultiIndexHitsCollectionController: NSObject, MultiIndexHitsController, HitsCollectionViewContainer {
+
+ public let collectionView: UICollectionView
+
+ public var hitsCollectionView: UICollectionView {
+ return collectionView
+ }
+
+ public weak var hitsSource: MultiIndexHitsSource?
+
+ public var dataSource: MultiIndexHitsCollectionViewDataSource?
+
+ public var delegate: MultiIndexHitsCollectionViewDelegate?
+
+ public init(collectionView: UICollectionView) {
+ self.collectionView = collectionView
+ }
+
+}
+#endif
diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift
similarity index 86%
rename from Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift
rename to Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift
index 20ac74ba..218ab870 100644
--- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift
+++ b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift
@@ -5,28 +5,31 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
-open class MultiIndexHitsCollectionViewDataSource: NSObject {
-
+@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol")
+open class MultiIndexHitsCollectionViewDataSource: NSObject, UICollectionViewDataSource {
+
private typealias CellConfigurator = (UICollectionView, Int) throws -> UICollectionViewCell
-
+
public weak var hitsSource: MultiIndexHitsSource?
-
+
private var cellConfigurators: [Int: CellConfigurator]
-
+
override init() {
cellConfigurators = [:]
super.init()
}
-
+
public func setCellConfigurator(forSection section: Int,
templateCellProvider: @escaping () -> UICollectionViewCell = { return .init() },
_ cellConfigurator: @escaping CollectionViewCellConfigurator) {
cellConfigurators[section] = { [weak self] (collectionView, row) in
-
+
guard let hitsSource = self?.hitsSource else {
Logger.missingHitsSourceWarning()
return .init()
@@ -39,11 +42,7 @@ open class MultiIndexHitsCollectionViewDataSource: NSObject {
return cellConfigurator(collectionView, hit, IndexPath(row: row, section: section))
}
}
-
-}
-extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource {
-
open func numberOfSections(in collectionView: UICollectionView) -> Int {
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
@@ -51,7 +50,7 @@ extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource {
}
return hitsSource.numberOfSections()
}
-
+
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
@@ -59,7 +58,7 @@ extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource {
}
return hitsSource.numberOfHits(inSection: section)
}
-
+
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cellConfigurator = cellConfigurators[indexPath.section] else {
Logger.missingCellConfiguratorWarning(forSection: indexPath.section)
@@ -72,5 +71,6 @@ extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource {
return .init()
}
}
-
+
}
+#endif
diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift
similarity index 79%
rename from Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift
rename to Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift
index f774b988..087e505d 100644
--- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift
+++ b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift
@@ -5,45 +5,44 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol")
-open class MultiIndexHitsCollectionViewDelegate: NSObject {
-
+@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol")
+open class MultiIndexHitsCollectionViewDelegate: NSObject, UICollectionViewDelegate {
+
typealias ClickHandler = (UICollectionView, Int) throws -> Void
-
+
public weak var hitsSource: MultiIndexHitsSource?
-
+
private var clickHandlers: [Int: ClickHandler]
-
+
public override init() {
clickHandlers = [:]
super.init()
}
-
+
public func setClickHandler(forSection section: Int, _ clickHandler: @escaping CollectionViewClickHandler) {
clickHandlers[section] = { [weak self] (collectionView, row) in
guard let delegate = self else { return }
-
+
guard let hitsSource = delegate.hitsSource else {
Logger.missingHitsSourceWarning()
return
}
-
+
guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else {
return
}
-
+
clickHandler(collectionView, hit, IndexPath(item: row, section: section))
-
+
}
}
-
-}
-extension MultiIndexHitsCollectionViewDelegate: UICollectionViewDelegate {
-
open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let clickHandler = clickHandlers[indexPath.section] else {
Logger.missingClickHandlerWarning(forSection: indexPath.section)
@@ -55,5 +54,6 @@ extension MultiIndexHitsCollectionViewDelegate: UICollectionViewDelegate {
Logger.error(error)
}
}
-
+
}
+#endif
diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableController.swift b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableController.swift
similarity index 78%
rename from Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableController.swift
rename to Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableController.swift
index 8316fc48..1a06c442 100644
--- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableController.swift
+++ b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableController.swift
@@ -6,43 +6,45 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
+@available(*, unavailable, message: "Use your own UITableViewController conforming to MultiIndexHitsController protocol")
public class MultiIndexHitsTableController: NSObject, HitsTableViewContainer, MultiIndexHitsController {
-
+
public var hitsTableView: UITableView {
return tableView
}
-
+
public let tableView: UITableView
-
+
public weak var hitsSource: MultiIndexHitsSource? {
didSet {
dataSource?.hitsSource = hitsSource
delegate?.hitsSource = hitsSource
}
}
-
- @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
+
public var dataSource: MultiIndexHitsTableViewDataSource? {
didSet {
dataSource?.hitsSource = hitsSource
tableView.dataSource = dataSource
}
}
-
- @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
+
public var delegate: MultiIndexHitsTableViewDelegate? {
didSet {
delegate?.hitsSource = hitsSource
tableView.delegate = delegate
}
}
-
+
public init(tableView: UITableView) {
self.tableView = tableView
}
-
+
}
+#endif
diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift
similarity index 84%
rename from Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift
rename to Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift
index bbf7df63..d5463832 100644
--- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift
+++ b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift
@@ -5,48 +5,44 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
-open class MultiIndexHitsTableViewDataSource: NSObject {
-
+@available(*, unavailable, message: "Use your own UITableViewController conforming to MultiIndexHitsController protocol")
+open class MultiIndexHitsTableViewDataSource: NSObject, UITableViewDataSource {
+
private typealias CellConfigurator = (UITableView, Int) throws -> UITableViewCell
-
+
public weak var hitsSource: MultiIndexHitsSource?
-
+
private var cellConfigurators: [Int: CellConfigurator]
-
+
public override init() {
cellConfigurators = [:]
super.init()
}
-
+
public func setCellConfigurator(forSection section: Int,
templateCellProvider: @escaping () -> UITableViewCell = { return .init() },
_ cellConfigurator: @escaping TableViewCellConfigurator) {
cellConfigurators[section] = { [weak self] (tableView, row) in
- guard let dataSource = self else {
- return .init()
- }
-
+
guard let hitsSource = self?.hitsSource else {
Logger.missingHitsSourceWarning()
return .init()
}
-
+
guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else {
return templateCellProvider()
}
-
+
return cellConfigurator(tableView, hit, IndexPath(row: row, section: section))
}
}
-
-}
-extension MultiIndexHitsTableViewDataSource: UITableViewDataSource {
-
open func numberOfSections(in tableView: UITableView) -> Int {
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
@@ -54,7 +50,7 @@ extension MultiIndexHitsTableViewDataSource: UITableViewDataSource {
}
return hitsSource.numberOfSections()
}
-
+
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let hitsSource = hitsSource else {
Logger.missingHitsSourceWarning()
@@ -62,7 +58,7 @@ extension MultiIndexHitsTableViewDataSource: UITableViewDataSource {
}
return hitsSource.numberOfHits(inSection: section)
}
-
+
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cellConfigurator = cellConfigurators[indexPath.section] else {
Logger.missingCellConfiguratorWarning(forSection: indexPath.section)
@@ -75,5 +71,6 @@ extension MultiIndexHitsTableViewDataSource: UITableViewDataSource {
return .init()
}
}
-
+
}
+#endif
diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift
similarity index 79%
rename from Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift
rename to Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift
index 8f3099cf..f10e7aa1 100644
--- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift
+++ b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift
@@ -5,23 +5,26 @@
// Created by Vladislav Fitc on 02/08/2019.
//
-import Foundation
+#if !InstantSearchCocoaPods
+import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
-@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol")
-open class MultiIndexHitsTableViewDelegate: NSObject {
-
+@available(*, unavailable, message: "Use your own UITableViewController conforming to MultiIndexHitsController protocol")
+open class MultiIndexHitsTableViewDelegate: NSObject, UITableViewDelegate {
+
typealias ClickHandler = (UITableView, Int) throws -> Void
-
+
public weak var hitsSource: MultiIndexHitsSource?
-
+
private var clickHandlers: [Int: ClickHandler]
-
+
public override init() {
clickHandlers = [:]
super.init()
}
-
+
public func setClickHandler(forSection section: Int, _ clickHandler: @escaping TableViewClickHandler) {
clickHandlers[section] = { [weak self] (tableView, row) in
guard let delegate = self else { return }
@@ -30,20 +33,16 @@ open class MultiIndexHitsTableViewDelegate: NSObject {
Logger.missingHitsSourceWarning()
return
}
-
+
guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else {
return
}
clickHandler(tableView, hit, IndexPath(item: row, section: section))
-
+
}
}
-
-}
-extension MultiIndexHitsTableViewDelegate: UITableViewDelegate {
-
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let clickHandler = clickHandlers[indexPath.section] else {
Logger.missingClickHandlerWarning(forSection: indexPath.section)
@@ -56,5 +55,6 @@ extension MultiIndexHitsTableViewDelegate: UITableViewDelegate {
return
}
}
-
+
}
+#endif
diff --git a/Sources/Controller/Numeric Comparator/NumericStepperController.swift b/Sources/InstantSearch/Numeric Comparator/NumericStepperController.swift
similarity index 90%
rename from Sources/Controller/Numeric Comparator/NumericStepperController.swift
rename to Sources/InstantSearch/Numeric Comparator/NumericStepperController.swift
index bb9a2f92..78619168 100644
--- a/Sources/Controller/Numeric Comparator/NumericStepperController.swift
+++ b/Sources/InstantSearch/Numeric Comparator/NumericStepperController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(macOS))
import UIKit
public class NumericStepperController: NumberController {
@@ -34,9 +36,10 @@ public class NumericStepperController: NumberController {
public init(stepper: UIStepper) {
self.stepper = stepper
}
-
+
public func invalidate() {
stepper.value = 0
}
-
+
}
+#endif
diff --git a/Sources/Controller/Numeric Comparator/NumericTextFieldController.swift b/Sources/InstantSearch/Numeric Comparator/NumericTextFieldController.swift
similarity index 91%
rename from Sources/Controller/Numeric Comparator/NumericTextFieldController.swift
rename to Sources/InstantSearch/Numeric Comparator/NumericTextFieldController.swift
index 29a824cc..0cf95b6d 100644
--- a/Sources/Controller/Numeric Comparator/NumericTextFieldController.swift
+++ b/Sources/InstantSearch/Numeric Comparator/NumericTextFieldController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(macOS) || os(tvOS))
import UIKit
public class NumericTextFieldController: NSObject, NumberController {
@@ -29,7 +31,9 @@ public class NumericTextFieldController: NSObject, NumberController {
public init(textField: UITextField) {
self.textField = textField
super.init()
+ #if (os(iOS) || os(macOS))
textField.addDoneCancelToolbar(onDone: (target: self, action: #selector(doneButtonTappedForMyNumericTextField)))
+ #endif
}
@objc func doneButtonTappedForMyNumericTextField() {
@@ -37,13 +41,13 @@ public class NumericTextFieldController: NSObject, NumberController {
self.computation?.just(value: intText)
textField.resignFirstResponder()
}
-
+
public func invalidate() {
textField.text = nil
}
}
-
+#if (os(iOS) || os(macOS))
extension UITextField {
func addDoneCancelToolbar(onDone: (target: Any, action: Selector)? = nil, onCancel: (target: Any, action: Selector)? = nil) {
let onCancel = onCancel ?? (target: self, action: #selector(cancelButtonTapped))
@@ -65,3 +69,5 @@ extension UITextField {
@objc func doneButtonTapped() { self.resignFirstResponder() }
@objc func cancelButtonTapped() { self.resignFirstResponder() }
}
+#endif
+#endif
diff --git a/Sources/Controller/QuerySuggestions/QuerySuggestionsViewController.swift b/Sources/InstantSearch/QuerySuggestions/QuerySuggestionsViewController.swift
similarity index 95%
rename from Sources/Controller/QuerySuggestions/QuerySuggestionsViewController.swift
rename to Sources/InstantSearch/QuerySuggestions/QuerySuggestionsViewController.swift
index 1fea3836..43841d28 100644
--- a/Sources/Controller/QuerySuggestions/QuerySuggestionsViewController.swift
+++ b/Sources/InstantSearch/QuerySuggestions/QuerySuggestionsViewController.swift
@@ -5,56 +5,58 @@
// Created by Vladislav Fitc on 20/01/2020.
//
-import Foundation
-import UIKit
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
+import UIKit
public class QuerySuggestionsViewController: UITableViewController, HitsController, QueryInputController {
-
+
public var onQuerySubmitted: ((String?) -> Void)?
public var onQueryChanged: ((String?) -> Void)?
-
+
public var hitsSource: HitsInteractor>?
-
+
public var didSelect: ((Hit) -> Void)?
-
+
public var isHighlightingInverted: Bool = false {
didSet {
tableView.reloadData()
}
}
-
+
let cellID = "suggestionCellID"
-
+
public override init(style: UITableView.Style) {
super.init(style: style)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
}
-
+
required init?(coder: NSCoder) {
super.init(coder: coder)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
}
-
+
public func scrollToTop() {
tableView.scrollToFirstNonEmptySection()
}
-
+
public func reload() {
tableView.reloadData()
}
-
+
public func setQuery(_ query: String?) {
// external query change doesn't affect
}
-
+
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return hitsSource?.numberOfHits() ?? 0
}
-
+
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellID) else { return .init() }
-
+
if let hightlightSuggestion = hitsSource?.hit(atIndex: indexPath.row)?.hightlightedString(forKey: "query") {
if let textLabel = cell.textLabel {
textLabel.attributedText = NSAttributedString(highlightedString: hightlightSuggestion, inverted: isHighlightingInverted, attributes: [.font: UIFont.boldSystemFont(ofSize: textLabel.font.pointSize)])
@@ -68,5 +70,6 @@ public class QuerySuggestionsViewController: UITableViewController, HitsControll
didSelect?(querySuggestionHit)
onQuerySubmitted?(querySuggestionHit.object.query)
}
-
+
}
+#endif
diff --git a/Sources/Controller/SearchBox/SearchBarController.swift b/Sources/InstantSearch/SearchBox/SearchBarController.swift
similarity index 91%
rename from Sources/Controller/SearchBox/SearchBarController.swift
rename to Sources/InstantSearch/SearchBox/SearchBarController.swift
index cf950baa..94a3fb90 100644
--- a/Sources/Controller/SearchBox/SearchBarController.swift
+++ b/Sources/InstantSearch/SearchBox/SearchBarController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
@available(iOS, deprecated: 13.0, message: "Use TextFieldController instead")
@@ -15,7 +17,7 @@ public class SearchBarController: NSObject, QueryInputController {
public var onQueryChanged: ((String?) -> Void)?
public var onQuerySubmitted: ((String?) -> Void)?
-
+
public let searchBar: UISearchBar
public init(searchBar: UISearchBar) {
@@ -24,11 +26,11 @@ public class SearchBarController: NSObject, QueryInputController {
setupSearchBar()
}
-
+
public func setQuery(_ query: String?) {
searchBar.text = query
}
-
+
private func setupSearchBar() {
searchBar.delegate = self
searchBar.returnKeyType = .search
@@ -47,3 +49,4 @@ extension SearchBarController: UISearchBarDelegate {
}
}
+#endif
diff --git a/Sources/Controller/SearchBox/TextFieldController.swift b/Sources/InstantSearch/SearchBox/TextFieldController.swift
similarity index 89%
rename from Sources/Controller/SearchBox/TextFieldController.swift
rename to Sources/InstantSearch/SearchBox/TextFieldController.swift
index 0569096e..c1234e91 100644
--- a/Sources/Controller/SearchBox/TextFieldController.swift
+++ b/Sources/InstantSearch/SearchBox/TextFieldController.swift
@@ -6,28 +6,32 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public class TextFieldController: NSObject, QueryInputController {
-
+
public var onQueryChanged: ((String?) -> Void)?
public var onQuerySubmitted: ((String?) -> Void)?
-
- let textField: UITextField
+
+ public let textField: UITextField
public init(textField: UITextField) {
self.textField = textField
super.init()
setupTextField()
}
-
+
+#if os(iOS)
@available(iOS 13.0, *)
public convenience init(searchBar: UISearchBar) {
self.init(textField: searchBar.searchTextField)
}
-
+#endif
+
public func setQuery(_ query: String?) {
textField.text = query
}
@@ -36,17 +40,18 @@ public class TextFieldController: NSObject, QueryInputController {
guard let searchText = textField.text else { return }
onQueryChanged?(searchText)
}
-
+
@objc func textFieldSubmitted(textField: UITextField) {
guard let searchText = textField.text else { return }
onQuerySubmitted?(searchText)
}
-
+
private func setupTextField() {
textField.returnKeyType = .search
textField.addTarget(self, action: #selector(textFieldTextChanged), for: .editingChanged)
textField.addTarget(self, action: #selector(textFieldTextChanged), for: .editingDidEnd)
textField.addTarget(self, action: #selector(textFieldSubmitted), for: .editingDidEndOnExit)
}
-
+
}
+#endif
diff --git a/Sources/Controller/Segmented/SegmentedController.swift b/Sources/InstantSearch/Segmented/SegmentedController.swift
similarity index 91%
rename from Sources/Controller/Segmented/SegmentedController.swift
rename to Sources/InstantSearch/Segmented/SegmentedController.swift
index beb145fa..179858a6 100644
--- a/Sources/Controller/Segmented/SegmentedController.swift
+++ b/Sources/InstantSearch/Segmented/SegmentedController.swift
@@ -6,18 +6,20 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public class SegmentedController: NSObject, SelectableSegmentController {
-
+
public typealias Key = Int
-
+
public let segmentedControl: UISegmentedControl
-
+
public var onClick: ((Int) -> Void)?
-
+
public init(segmentedControl: UISegmentedControl) {
self.segmentedControl = segmentedControl
super.init()
@@ -27,19 +29,20 @@ public class SegmentedController: NSObject, SelectableSegment
public func setSelected(_ selected: Int?) {
segmentedControl.selectedSegmentIndex = selected ?? UISegmentedControl.noSegment
}
-
+
public func setItems(items: [Int: String]) {
segmentedControl.removeAllSegments()
-
+
for item in items {
segmentedControl.insertSegment(withTitle: item.value, at: item.key, animated: false)
}
}
-
+
@objc private func didSelectSegment(_ segmentedControl: UISegmentedControl) {
if segmentedControl.selectedSegmentIndex != UISegmentedControl.noSegment {
onClick?(segmentedControl.selectedSegmentIndex)
}
}
-
+
}
+#endif
diff --git a/Sources/Controller/Selectable/FilterSwitchController.swift b/Sources/InstantSearch/Selectable/FilterSwitchController.swift
similarity index 88%
rename from Sources/Controller/Selectable/FilterSwitchController.swift
rename to Sources/InstantSearch/Selectable/FilterSwitchController.swift
index 71c38c7b..d2342276 100644
--- a/Sources/Controller/Selectable/FilterSwitchController.swift
+++ b/Sources/InstantSearch/Selectable/FilterSwitchController.swift
@@ -6,33 +6,36 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(macOS))
import UIKit
public class FilterSwitchController: SelectableController {
-
+
public typealias Item = F
-
+
public let `switch`: UISwitch
-
+
public var onClick: ((Bool) -> Void)?
-
+
public init(`switch`: UISwitch) {
self.switch = `switch`
`switch`.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
}
-
+
@objc func didToggleSwitch(_ switch: UISwitch) {
onClick?(`switch`.isOn)
}
-
+
public func setSelected(_ isSelected: Bool) {
self.switch.isOn = isSelected
}
-
+
public func setItem(_ item: F) {
-
+
}
-
+
}
+#endif
diff --git a/Sources/Controller/Selectable/SelectableFilterButtonController.swift b/Sources/InstantSearch/Selectable/SelectableFilterButtonController.swift
similarity index 88%
rename from Sources/Controller/Selectable/SelectableFilterButtonController.swift
rename to Sources/InstantSearch/Selectable/SelectableFilterButtonController.swift
index 4c351de3..3710da1e 100644
--- a/Sources/Controller/Selectable/SelectableFilterButtonController.swift
+++ b/Sources/InstantSearch/Selectable/SelectableFilterButtonController.swift
@@ -6,34 +6,37 @@
// Copyright © 2019 Algolia. All rights reserved.
//b
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public class SelectableFilterButtonController: SelectableController {
-
+
public typealias Item = F
-
+
public let button: UIButton
-
+
public var onClick: ((Bool) -> Void)?
-
+
public init(button: UIButton) {
self.button = button
button.addTarget(self, action: #selector(didToggleButton), for: .touchUpInside)
}
-
+
@objc func didToggleButton(_ button: UIButton) {
onClick?(!button.isSelected)
}
-
+
public func setSelected(_ isSelected: Bool) {
self.button.isSelected = isSelected
}
-
+
public func setItem(_ item: F) {
let title = DefaultPresenter.Filter.present(Filter(item))
button.setTitle(title, for: .normal)
}
-
+
}
+#endif
diff --git a/Sources/Controller/Sort By/SelectIndexController.swift b/Sources/InstantSearch/Sort By/SelectIndexController.swift
similarity index 90%
rename from Sources/Controller/Sort By/SelectIndexController.swift
rename to Sources/InstantSearch/Sort By/SelectIndexController.swift
index c77d0392..ec4b2bda 100644
--- a/Sources/Controller/Sort By/SelectIndexController.swift
+++ b/Sources/InstantSearch/Sort By/SelectIndexController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
public class SelectIndexController: NSObject, SelectableSegmentController {
@@ -38,3 +40,4 @@ public class SelectIndexController: NSObject, SelectableSegmentController {
}
}
+#endif
diff --git a/Sources/Controller/Stats/LabelStatsController.swift b/Sources/InstantSearch/Stats/LabelStatsController.swift
similarity index 88%
rename from Sources/Controller/Stats/LabelStatsController.swift
rename to Sources/InstantSearch/Stats/LabelStatsController.swift
index 726ae438..f2c3ae8e 100644
--- a/Sources/Controller/Stats/LabelStatsController.swift
+++ b/Sources/InstantSearch/Stats/LabelStatsController.swift
@@ -6,8 +6,10 @@
// Copyright © 2019 Algolia. All rights reserved.
//
-import Foundation
+#if !InstantSearchCocoaPods
import InstantSearchCore
+#endif
+#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS))
import UIKit
extension Optional where Wrapped == Bool {
@@ -30,7 +32,7 @@ public class LabelStatsController: StatsTextController {
public init (label: UILabel) {
self.label = label
}
-
+
public func setItem(_ item: String?) {
label.text = item
}
@@ -38,15 +40,16 @@ public class LabelStatsController: StatsTextController {
}
public class AttributedLabelStatsController: ItemAttributedTextController {
-
+
public let label: UILabel
-
+
public init (label: UILabel) {
self.label = label
}
-
+
public func setItem(_ item: NSAttributedString?) {
label.attributedText = item
}
}
+#endif
diff --git a/Sources/InstantSearchCore/Common/AsyncOperation.swift b/Sources/InstantSearchCore/Common/AsyncOperation.swift
new file mode 100644
index 00000000..822b27d1
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/AsyncOperation.swift
@@ -0,0 +1,66 @@
+//
+// AsyncOperation.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 04/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+open class AsyncOperation: Operation {
+
+ public enum State: String {
+ case ready, executing, finished
+
+ fileprivate var keyPath: String {
+ return "is" + rawValue.capitalized
+ }
+ }
+
+ public var state = State.ready {
+ willSet {
+ willChangeValue(forKey: newValue.keyPath)
+ willChangeValue(forKey: state.keyPath)
+ }
+ didSet {
+ didChangeValue(forKey: oldValue.keyPath)
+ didChangeValue(forKey: state.keyPath)
+ }
+ }
+
+}
+
+extension AsyncOperation {
+ // NSOperation Overrides
+ override open var isReady: Bool {
+ return super.isReady && state == .ready
+ }
+
+ override open var isExecuting: Bool {
+ return state == .executing
+ }
+
+ override open var isFinished: Bool {
+ return state == .finished
+ }
+
+ override open var isAsynchronous: Bool {
+ return true
+ }
+
+ override open func start() {
+ if isCancelled {
+ state = .finished
+ return
+ }
+
+ main()
+ state = .executing
+ }
+
+ open override func cancel() {
+ state = .finished
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Attribute.swift b/Sources/InstantSearchCore/Common/Attribute.swift
new file mode 100644
index 00000000..d2fc663a
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Attribute.swift
@@ -0,0 +1,13 @@
+// Attribute.swift
+// AlgoliaSearch
+//
+// Created by Guy Daher on 10/12/2018.
+// Copyright © 2018 Algolia. All rights reserved.
+//
+import Foundation
+
+extension Attribute {
+
+ public static let tags = Attribute("_tags")
+
+}
diff --git a/Sources/InstantSearchCore/Common/Connection.swift b/Sources/InstantSearchCore/Common/Connection.swift
new file mode 100644
index 00000000..0c0cb135
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Connection.swift
@@ -0,0 +1,16 @@
+//
+// Connection.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 19/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol Connection {
+
+ func connect()
+ func disconnect()
+
+}
diff --git a/Sources/InstantSearchCore/Common/Constants.swift b/Sources/InstantSearchCore/Common/Constants.swift
new file mode 100644
index 00000000..e80d98d7
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Constants.swift
@@ -0,0 +1,27 @@
+//
+// Constants.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 07/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public struct Constants {
+ public struct Defaults {
+
+ // Hits
+ public static let hitsPerPage: UInt = 20
+ public static let infiniteScrolling: InfiniteScrolling = .on(withOffset: 5)
+ public static let showItemsOnEmptyQuery: Bool = true
+
+ // Refinement
+ public static let operatorRefinement = "or"
+ public static let refinementOperator: RefinementOperator = .or
+ public static let refinedFirst = true
+
+ public static let limit = 10
+ public static let areMultipleSelectionsAllowed = false
+ }
+}
diff --git a/Sources/InstantSearchCore/Common/Event/EventInteractor.swift b/Sources/InstantSearchCore/Common/Event/EventInteractor.swift
new file mode 100644
index 00000000..82f5370a
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Event/EventInteractor.swift
@@ -0,0 +1,16 @@
+//
+// EventInteractor.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 10/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol EventInteractor {
+ associatedtype Arg
+
+ var onTriggered: Observer { get }
+
+}
diff --git a/Sources/InstantSearchCore/Common/GeoLocation.swift b/Sources/InstantSearchCore/Common/GeoLocation.swift
new file mode 100644
index 00000000..3407efe9
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/GeoLocation.swift
@@ -0,0 +1,39 @@
+//
+// GeoLocation.swift
+// InstantSearchCore-iOS
+//
+// Created by Vladislav Fitc on 05/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public struct GeoLocation: Codable, RawRepresentable {
+
+ public typealias RawValue = String
+
+ public let latitude: Double
+ public let longitude: Double
+
+ public init(latitude: Double, longitude: Double) {
+ self.latitude = latitude
+ self.longitude = longitude
+ }
+
+ public init?(rawValue: RawValue) {
+ let components = rawValue.split(separator: ",")
+ guard
+ components.count == 2,
+ let latitude = Double(components[0]),
+ let longitude = Double(components[1]) else
+ {
+ return nil
+ }
+ self.init(latitude: latitude, longitude: longitude)
+ }
+
+ public var rawValue: String {
+ return "\(latitude),\(longitude)"
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Geolocated.swift b/Sources/InstantSearchCore/Common/Geolocated.swift
new file mode 100644
index 00000000..2039e9a4
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Geolocated.swift
@@ -0,0 +1,13 @@
+//
+// Geolocated.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/10/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol Geolocated {
+ var geolocation: Point? { get }
+}
diff --git a/Sources/InstantSearchCore/Common/Hit+Place.swift b/Sources/InstantSearchCore/Common/Hit+Place.swift
new file mode 100644
index 00000000..f1b1ee9b
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Hit+Place.swift
@@ -0,0 +1,84 @@
+//
+// Hit+Place.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 24/10/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension Hit {
+
+ func getBestHighlightedForm(from highlightResults: [HighlightResult]) -> HighlightedString? {
+
+ guard let defaultValue = highlightResults.first?.value.taggedString.input else { return nil }
+
+ let bestAttributesOrder = highlightResults
+ .filter { $0.matchLevel != .none }
+ .enumerated()
+ .sorted { lhs, rhs in
+ // If matched words count is the same, use the earliest occurency in the list
+ guard lhs.element.matchedWords.count != rhs.element.matchedWords.count else {
+ return lhs.offset < rhs.offset
+ }
+ return lhs.element.matchedWords.count > rhs.element.matchedWords.count
+ }
+ .map { $0.offset }
+ .first
+
+ guard let theBestAttributeIndex = bestAttributesOrder else { return HighlightedString(string: defaultValue) }
+
+ return highlightResults[theBestAttributeIndex].value
+
+ }
+
+ func getBestHighlightedForm(forKey key: String) -> HighlightedString? {
+
+ guard case .dictionary(let dictionary) = highlightResult else {
+ return nil
+ }
+
+ let highlightResults: [HighlightResult]
+
+ switch dictionary[key] {
+ case .array(let highlightResultsList):
+ highlightResults = highlightResultsList.compactMap {
+ if case .value(let val) = $0 {
+ return val
+ } else {
+ return nil
+ }
+ }
+
+ case .value(let value):
+ highlightResults = [value]
+
+ default:
+ return nil
+ }
+
+ return getBestHighlightedForm(from: highlightResults)
+
+ }
+
+}
+
+extension Hit: CustomStringConvertible where T == Place {
+
+ public var description: String {
+ let cityKey = object.isCity! ? "locale_names" : "city"
+ let country = getBestHighlightedForm(forKey: "country")
+ let county = getBestHighlightedForm(forKey: "county")
+ let city = getBestHighlightedForm(forKey: cityKey)
+ let streetName = object.isCity! ? nil : getBestHighlightedForm(forKey: "locale_names")
+
+ return [streetName, city, county, country]
+ .compactMap { $0 }
+ .filter { !$0.taggedString.input.isEmpty }
+ .map { $0.taggedString.output }
+ .joined(separator: ", ")
+
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/IndexQueryState.swift b/Sources/InstantSearchCore/Common/IndexQueryState.swift
new file mode 100644
index 00000000..19aaf900
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/IndexQueryState.swift
@@ -0,0 +1,36 @@
+//
+// IndexQueryState.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 27/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+@_exported import AlgoliaSearchClient
+/// Structure containing all necessary components to perform a search
+
+public struct IndexQueryState {
+
+ /// Index in which search will be performed
+ public var indexName: IndexName
+
+ /// Query describing a search request
+ public var query: Query
+
+ public init(indexName: IndexName, query: Query = .init()) {
+ self.indexName = indexName
+ self.query = query
+ }
+
+}
+
+extension IndexQueryState: Builder {}
+
+extension Array where Element == IndexQueryState {
+
+ init(indices: [AlgoliaSearchClient.Index], query: Query = .init()) {
+ self = indices.map { IndexQueryState(indexName: $0.name, query: query) }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Item/ItemController.swift b/Sources/InstantSearchCore/Common/Item/ItemController.swift
new file mode 100644
index 00000000..f4ebae09
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Item/ItemController.swift
@@ -0,0 +1,25 @@
+//
+// ItemController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 31/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol ItemController: class {
+
+ associatedtype Item
+
+ func setItem(_ item: Item)
+ func invalidate()
+
+}
+
+public extension ItemController {
+
+ func invalidate() {
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Item/ItemInteractor+Controller.swift b/Sources/InstantSearchCore/Common/Item/ItemInteractor+Controller.swift
new file mode 100644
index 00000000..b7eaeb11
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Item/ItemInteractor+Controller.swift
@@ -0,0 +1,42 @@
+//
+// ItemInteractor+Controller.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension ItemInteractor {
+
+ struct ControllerConnection: Connection where Controller.Item == Output {
+
+ public let interactor: ItemInteractor
+ public let controller: Controller
+ public let presenter: Presenter-
+
+ public func connect() {
+ interactor.onItemChanged.subscribePast(with: controller) { controller, item in
+ controller.setItem(self.presenter(item))
+ }.onQueue(.main)
+ }
+
+ public func disconnect() {
+ interactor.onItemChanged.cancelSubscription(for: controller)
+ }
+
+ }
+
+}
+
+public extension ItemInteractor {
+
+ @discardableResult func connectController
(_ controller: Controller,
+ presenter: @escaping Presenter- ) -> ControllerConnection
{
+ let connection = ControllerConnection(interactor: self, controller: controller, presenter: presenter)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Item/ItemInteractor.swift b/Sources/InstantSearchCore/Common/Item/ItemInteractor.swift
new file mode 100644
index 00000000..98ef4fa7
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Item/ItemInteractor.swift
@@ -0,0 +1,26 @@
+//
+// ItemInteractor.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 31/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class ItemInteractor- {
+
+ public var item: Item {
+ didSet {
+ onItemChanged.fire(item)
+ }
+ }
+
+ public let onItemChanged: Observer
-
+
+ init(item: Item) {
+ self.item = item
+ self.onItemChanged = .init()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Number/Boundable.swift b/Sources/InstantSearchCore/Common/Number/Boundable.swift
new file mode 100644
index 00000000..d47ae2e7
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Number/Boundable.swift
@@ -0,0 +1,15 @@
+//
+// NumberRangeInteractor+Searcher.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 14/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol Boundable: class {
+ associatedtype Number: Comparable & DoubleRepresentable
+
+ func applyBounds(bounds: ClosedRange
?)
+}
diff --git a/Sources/InstantSearchCore/Common/Number/DoubleRepresentable.swift b/Sources/InstantSearchCore/Common/Number/DoubleRepresentable.swift
new file mode 100644
index 00000000..e44c2425
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Number/DoubleRepresentable.swift
@@ -0,0 +1,39 @@
+//
+// DoubleRepresentable.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 14/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol DoubleRepresentable {
+
+ init(_ double: Double)
+ func toDouble() -> Double
+}
+
+extension Int: DoubleRepresentable {
+ public func toDouble() -> Double {
+ return Double(self)
+ }
+}
+
+extension UInt: DoubleRepresentable {
+ public func toDouble() -> Double {
+ return Double(self)
+ }
+}
+
+extension Float: DoubleRepresentable {
+ public func toDouble() -> Double {
+ return Double(self)
+ }
+}
+
+extension Double: DoubleRepresentable {
+ public func toDouble() -> Double {
+ return Double(self)
+ }
+}
diff --git a/Sources/InstantSearchCore/Common/Point+CoreLocation.swift b/Sources/InstantSearchCore/Common/Point+CoreLocation.swift
new file mode 100644
index 00000000..e7ab7f44
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Point+CoreLocation.swift
@@ -0,0 +1,35 @@
+//
+// Point+CoreLocation.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 28/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+import CoreLocation
+
+public extension Point {
+
+ init(_ coordinate: CLLocationCoordinate2D) {
+ self.init(latitude: coordinate.latitude, longitude: coordinate.longitude)
+ }
+
+}
+
+public extension CLLocationCoordinate2D {
+
+ init(_ geolocation: Point) {
+ self.init(latitude: geolocation.latitude, longitude: geolocation.longitude)
+ }
+
+}
+
+public extension CLLocation {
+
+ convenience init(_ geolocation: Point) {
+ self.init(latitude: geolocation.latitude, longitude: geolocation.longitude)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/Presenter/Presenter.swift b/Sources/InstantSearchCore/Common/Presenter/Presenter.swift
new file mode 100644
index 00000000..f4b9338f
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Presenter/Presenter.swift
@@ -0,0 +1,11 @@
+//
+// Presenter.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 12/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias Presenter = (I) -> O
diff --git a/Sources/InstantSearchCore/Common/QuerySuggestion.swift b/Sources/InstantSearchCore/Common/QuerySuggestion.swift
new file mode 100644
index 00000000..548871b0
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/QuerySuggestion.swift
@@ -0,0 +1,16 @@
+//
+// QuerySuggestion.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 20/01/2020.
+// Copyright © 2020 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public struct QuerySuggestion: Codable {
+
+ public let query: String
+ public let popularity: Int
+
+}
diff --git a/Sources/InstantSearchCore/Common/Reloadable.swift b/Sources/InstantSearchCore/Common/Reloadable.swift
new file mode 100644
index 00000000..88f028db
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Reloadable.swift
@@ -0,0 +1,15 @@
+//
+// Reloadable.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 13/12/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol Reloadable {
+
+ func reload()
+
+}
diff --git a/Sources/InstantSearchCore/Common/ResultUpdatable.swift b/Sources/InstantSearchCore/Common/ResultUpdatable.swift
new file mode 100644
index 00000000..b609877c
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/ResultUpdatable.swift
@@ -0,0 +1,23 @@
+//
+// ResultUpdatable.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 12/12/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol ResultUpdatable {
+
+ /// Result type
+ associatedtype Result
+
+ /// Triggered once result is updated
+ var onResultsUpdated: Observer { get }
+
+ /// Updates result
+ /// - returns: Operation encapsulating result update
+ func update(_ result: Result) -> Operation
+
+}
diff --git a/Sources/InstantSearchCore/Common/Selectable/SelectableController.swift b/Sources/InstantSearchCore/Common/Selectable/SelectableController.swift
new file mode 100644
index 00000000..32ba7774
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Selectable/SelectableController.swift
@@ -0,0 +1,17 @@
+//
+// SelectableController.swift
+// InstantSearchCore-iOS
+//
+// Created by Vladislav Fitc on 03/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol SelectableController: ItemController {
+
+ var onClick: ((Bool) -> Void)? { get set }
+
+ func setSelected(_ isSelected: Bool)
+
+}
diff --git a/Sources/InstantSearchCore/Common/Selectable/SelectableInteractor.swift b/Sources/InstantSearchCore/Common/Selectable/SelectableInteractor.swift
new file mode 100644
index 00000000..a94ff464
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/Selectable/SelectableInteractor.swift
@@ -0,0 +1,33 @@
+//
+// SelectableInteractor.swift
+// InstantSearchCore-iOS
+//
+// Created by Vladislav Fitc on 03/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class SelectableInteractor- : ItemInteractor
- {
+
+ public var isSelected: Bool {
+ didSet {
+ onSelectedChanged.fire(isSelected)
+ }
+ }
+
+ public let onSelectedChanged: Observer
+ public let onSelectedComputed: Observer
+
+ public override init(item: Item) {
+ self.isSelected = false
+ self.onSelectedChanged = .init()
+ self.onSelectedComputed = .init()
+ super.init(item: item)
+ }
+
+ public func computeIsSelected(selecting: Bool) {
+ onSelectedComputed.fire(selecting)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/SelectableList/SelectableListController.swift b/Sources/InstantSearchCore/Common/SelectableList/SelectableListController.swift
new file mode 100644
index 00000000..69a56cda
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/SelectableList/SelectableListController.swift
@@ -0,0 +1,19 @@
+//
+// SelectableListController.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 26/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol SelectableListController: class, Reloadable {
+
+ associatedtype Item
+
+ var onClick: ((Item) -> Void)? { get set }
+
+ func setSelectableItems(selectableItems: [SelectableItem- ])
+
+}
diff --git a/Sources/InstantSearchCore/Common/SelectableList/SelectableListInteractor.swift b/Sources/InstantSearchCore/Common/SelectableList/SelectableListInteractor.swift
new file mode 100644
index 00000000..f67b89c4
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/SelectableList/SelectableListInteractor.swift
@@ -0,0 +1,71 @@
+//
+// SelectableListInteractor.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 18/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public enum SelectionMode {
+ case single
+ case multiple
+}
+
+public class SelectableListInteractor
{
+
+ public var items: [Item] {
+ didSet {
+ if oldValue != items {
+ onItemsChanged.fire(items)
+ }
+ }
+ }
+
+ public var selections: Set {
+ didSet {
+ if oldValue != selections {
+ onSelectionsChanged.fire(selections)
+ }
+ }
+ }
+
+ public let onItemsChanged: Observer<[Item]>
+ public let onSelectionsChanged: Observer>
+ public let onSelectionsComputed: Observer>
+
+ public let selectionMode: SelectionMode
+
+ public init(items: [Item] = [], selectionMode: SelectionMode) {
+ self.items = items
+ self.selections = []
+ self.onItemsChanged = .init()
+ self.onSelectionsChanged = .init()
+ self.onSelectionsComputed = .init()
+ self.selectionMode = selectionMode
+ }
+
+ public func computeSelections(selectingItemForKey key: Key) {
+
+ let computedSelections: Set
+
+ switch (selectionMode, selections.contains(key)) {
+ case (.single, true):
+ computedSelections = []
+
+ case (.single, false):
+ computedSelections = [key]
+
+ case (.multiple, true):
+ computedSelections = selections.subtracting([key])
+
+ case (.multiple, false):
+ computedSelections = selections.union([key])
+ }
+
+ onSelectionsComputed.fire(computedSelections)
+
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentController.swift b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentController.swift
new file mode 100644
index 00000000..d685d4e7
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentController.swift
@@ -0,0 +1,20 @@
+//
+// SelectableSegmentController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 13/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol SelectableSegmentController: class {
+
+ associatedtype SegmentKey: Hashable
+
+ var onClick: ((SegmentKey) -> Void)? { get set }
+
+ func setSelected(_ selected: SegmentKey?)
+ func setItems(items: [SegmentKey: String])
+
+}
diff --git a/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentInteractor.swift b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentInteractor.swift
new file mode 100644
index 00000000..3b79b443
--- /dev/null
+++ b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentInteractor.swift
@@ -0,0 +1,42 @@
+//
+// SelectableSegmentInteractor.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class SelectableSegmentInteractor {
+
+ public var items: [SegmentKey: Segment] {
+ didSet {
+ onItemsChanged.fire(items)
+ }
+ }
+
+ public var selected: SegmentKey? {
+ didSet {
+ onSelectedChanged.fire(selected)
+ }
+ }
+
+ public let onItemsChanged: Observer<[SegmentKey: Segment]>
+ public let onSelectedChanged: Observer
+ public let onSelectedComputed: Observer
+
+ public init(items: [SegmentKey: Segment]) {
+ self.items = items
+ self.selected = .none
+ self.onItemsChanged = .init()
+ self.onSelectedChanged = .init()
+ self.onSelectedComputed = .init()
+ onItemsChanged.fire(items)
+ }
+
+ public func computeSelected(selecting keyToSelect: SegmentKey?) {
+ onSelectedComputed.fire(keyToSelect)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersConnector.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersConnector.swift
new file mode 100644
index 00000000..8429af81
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersConnector.swift
@@ -0,0 +1,37 @@
+//
+// CurrentFiltersConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 29/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class CurrentFiltersConnector: Connection {
+
+ public let filterState: FilterState
+ public let groupIDs: Set?
+ public let interactor: CurrentFiltersInteractor
+
+ public let filterStateConnection: Connection
+
+ public init(filterState: FilterState,
+ groupIDs: Set? = nil,
+ interactor: CurrentFiltersInteractor = .init()) {
+ self.filterState = filterState
+ self.groupIDs = groupIDs
+ self.interactor = interactor
+ self.filterStateConnection = interactor.connectFilterState(filterState, filterGroupIDs: groupIDs)
+
+ }
+
+ public func connect() {
+ filterStateConnection.connect()
+ }
+
+ public func disconnect() {
+ filterStateConnection.disconnect()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor+FilterState.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor+FilterState.swift
new file mode 100644
index 00000000..a7b25e43
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor+FilterState.swift
@@ -0,0 +1,91 @@
+//
+// CurrentFiltersInteractor+FilterState.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 19/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension CurrentFiltersInteractor {
+
+ struct FilterStateConnection: Connection {
+
+ public let interactor: CurrentFiltersInteractor
+ public let filterState: FilterState
+ public let filterGroupIDs: Set?
+
+ public init(interactor: CurrentFiltersInteractor,
+ filterState: FilterState,
+ filterGroupIDs: Set? = nil) {
+ self.interactor = interactor
+ self.filterState = filterState
+ self.filterGroupIDs = filterGroupIDs
+ }
+
+ public func connect() {
+
+ let filterGroupIDs = self.filterGroupIDs
+
+ filterState.onChange.subscribePast(with: interactor) { [weak filterState] interactor, _ in
+ guard let filterState = filterState else { return }
+ if let filterGroupIDs = filterGroupIDs {
+ interactor.items = filterState.getFiltersAndID().filter { filterGroupIDs.contains($0.id) }
+ } else {
+ interactor.items = filterState.getFiltersAndID()
+ }
+ }
+
+ interactor.onItemsComputed.subscribePast(with: filterState) { filterState, items in
+
+ if let filterGroupIDs = filterGroupIDs {
+ filterState.filters.removeAll(fromGroupWithIDs: Array(filterGroupIDs))
+ items.forEach({ (filterAndID) in
+ filterState.filters.add(filterAndID.filter.filter, toGroupWithID: filterAndID.id)
+ })
+ } else {
+ filterState.filters.removeAll()
+ items.forEach({ (filterAndID) in
+ filterState.filters.add(filterAndID.filter.filter, toGroupWithID: filterAndID.id)
+ })
+ }
+
+ filterState.notifyChange()
+ }
+
+ }
+
+ public func disconnect() {
+ filterState.onChange.cancelSubscription(for: interactor)
+ interactor.onItemsComputed.cancelSubscription(for: filterState)
+ }
+
+ }
+
+}
+
+public extension CurrentFiltersInteractor {
+
+ @discardableResult func connectFilterState(_ filterState: FilterState,
+ filterGroupIDs: Set? = nil) -> FilterStateConnection {
+ let connection = FilterStateConnection(interactor: self, filterState: filterState, filterGroupIDs: filterGroupIDs)
+ connection.connect()
+ return connection
+ }
+
+ func connectFilterState(_ filterState: FilterState,
+ filterGroupID: FilterGroup.ID?) -> FilterStateConnection {
+ if let filterGroupID = filterGroupID {
+ return connectFilterState(filterState, filterGroupIDs: Set([filterGroupID]))
+ } else {
+ return connectFilterState(filterState)
+ }
+ }
+
+ func connectFilterState(_ filterState: FilterState,
+ filterGroupID: FilterGroup.ID) -> FilterStateConnection {
+ return connectFilterState(filterState, filterGroupIDs: Set([filterGroupID]))
+ }
+
+}
diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor.swift
new file mode 100644
index 00000000..29b5e6a4
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor.swift
@@ -0,0 +1,23 @@
+//
+// CurrentFiltersInteractor.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 12/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias CurrentFiltersInteractor = ItemsListInteractor
+
+public struct FilterAndID: Hashable {
+ public let filter: Filter
+ public let id: FilterGroup.ID
+ public var text: String
+
+ public init(filter: Filter, id: FilterGroup.ID, text: String = "") {
+ self.filter = filter
+ self.id = id
+ self.text = text
+ }
+}
diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersListController.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersListController.swift
new file mode 100644
index 00000000..90836f43
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersListController.swift
@@ -0,0 +1,11 @@
+//
+// CurrentFiltersListController.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 12/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol CurrentFiltersListController: ItemListController where Item == FilterAndID {}
diff --git a/Sources/InstantSearchCore/CurrentFilters/ItemListController.swift b/Sources/InstantSearchCore/CurrentFilters/ItemListController.swift
new file mode 100644
index 00000000..80be3af2
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/ItemListController.swift
@@ -0,0 +1,25 @@
+//
+// CurrentFiltersController.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 12/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol ItemListController: class {
+
+ associatedtype Item: Hashable
+
+ func setItems(_ items: [Item])
+
+ // TODO: Potentially we could change from Item to a Int which is position of item in list.
+ // It is enough to identify the items in interactor, so in that way we only pass the
+ // Filter without the ID
+ var onRemoveItem: ((Item) -> Void)? { get set }
+
+ func reload()
+}
+
+public protocol CurrentFiltersController: ItemListController where Item == FilterAndID {}
diff --git a/Sources/InstantSearchCore/CurrentFilters/ItemListInteractor+Controller.swift b/Sources/InstantSearchCore/CurrentFilters/ItemListInteractor+Controller.swift
new file mode 100644
index 00000000..d05b8ebd
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/ItemListInteractor+Controller.swift
@@ -0,0 +1,62 @@
+//
+// ItemListInteractor+Controller.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 19/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension ItemsListInteractor {
+
+ struct ControllerConnection: Connection where Controller.Item == Item, Item == FilterAndID {
+
+ public let interactor: ItemsListInteractor
+ public let controller: Controller
+ public let presenter: Presenter
+
+ public init(interactor: ItemsListInteractor,
+ controller: Controller,
+ presenter: @escaping Presenter = DefaultPresenter.Filter.present) {
+ self.interactor = interactor
+ self.controller = controller
+ self.presenter = presenter
+ }
+
+ public func connect() {
+
+ controller.onRemoveItem = { [weak interactor] item in
+ let filterAndID = FilterAndID(filter: item.filter, id: item.id)
+ interactor?.remove(item: filterAndID)
+ }
+
+ let presenter = self.presenter
+
+ interactor.onItemsChanged.subscribePast(with: controller) { controller, items in
+ let itemsWithPresenterApplied = items.map { FilterAndID(filter: $0.filter, id: $0.id, text: presenter($0.filter))}
+ controller.setItems(itemsWithPresenterApplied)
+ controller.reload()
+ }.onQueue(.main)
+
+ }
+
+ public func disconnect() {
+ controller.onRemoveItem = .none
+ interactor.onItemsChanged.cancelSubscription(for: controller)
+ }
+
+ }
+
+}
+
+public extension ItemsListInteractor {
+
+ @discardableResult func connectController(_ controller: C,
+ presenter: @escaping Presenter = DefaultPresenter.Filter.present) -> ControllerConnection where C.Item == Item, Item == FilterAndID {
+ let connection = ControllerConnection(interactor: self, controller: controller, presenter: presenter)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/CurrentFilters/ItemsListInteractor.swift b/Sources/InstantSearchCore/CurrentFilters/ItemsListInteractor.swift
new file mode 100644
index 00000000..c32d7633
--- /dev/null
+++ b/Sources/InstantSearchCore/CurrentFilters/ItemsListInteractor.swift
@@ -0,0 +1,38 @@
+//
+// ItemsListInteractor.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 12/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class ItemsListInteractor {
+
+ public var items: Set- {
+ didSet {
+ if oldValue != items {
+ onItemsChanged.fire(items)
+ }
+ }
+ }
+
+ public let onItemsChanged: Observer
>
+ public let onItemsComputed: Observer>
+
+ public init(items: Set- = []) {
+ self.items = items
+ self.onItemsChanged = .init()
+ self.onItemsComputed = .init()
+ }
+
+ public func remove(item: Item) {
+ onItemsComputed.fire(items.subtracting([item]))
+ }
+
+ public func add(item: Item) {
+ onItemsComputed.fire(items.union([item]))
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Extensions/Optional+Collection.swift b/Sources/InstantSearchCore/Extensions/Optional+Collection.swift
new file mode 100644
index 00000000..630c0b72
--- /dev/null
+++ b/Sources/InstantSearchCore/Extensions/Optional+Collection.swift
@@ -0,0 +1,22 @@
+//
+// Optional+Collection.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 12/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension Optional where Wrapped: Collection {
+
+ var isNilOrEmpty: Bool {
+ switch self {
+ case .none:
+ return true
+ case .some(let collection):
+ return collection.isEmpty
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Extensions/Optional+String.swift b/Sources/InstantSearchCore/Extensions/Optional+String.swift
new file mode 100644
index 00000000..e784bfac
--- /dev/null
+++ b/Sources/InstantSearchCore/Extensions/Optional+String.swift
@@ -0,0 +1,22 @@
+//
+// Optional+String.swift
+// InstantSearchCore-iOS
+//
+// Created by Vladislav Fitc on 13/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension Optional where Wrapped == String {
+
+ var isNilOrEmpty: Bool {
+ switch self {
+ case .some(let string):
+ return string.isEmpty
+ case .none:
+ return true
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Extensions/Query+Facets.swift b/Sources/InstantSearchCore/Extensions/Query+Facets.swift
new file mode 100644
index 00000000..ca05b869
--- /dev/null
+++ b/Sources/InstantSearchCore/Extensions/Query+Facets.swift
@@ -0,0 +1,17 @@
+//
+// Query+Facets.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension Query {
+
+ mutating func updateQueryFacets(with attribute: Attribute) {
+ facets = (facets ?? []).union([attribute])
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Extensions/Result+ConvenientInit.swift b/Sources/InstantSearchCore/Extensions/Result+ConvenientInit.swift
new file mode 100644
index 00000000..e19e1012
--- /dev/null
+++ b/Sources/InstantSearchCore/Extensions/Result+ConvenientInit.swift
@@ -0,0 +1,50 @@
+//
+// Result.swift
+// InstantSearchCore-iOS
+//
+// Created by Guy Daher on 07/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension Result where Failure == Error {
+
+ init(value: Success?, error: Failure?) {
+ switch (value, error) {
+ case (_, .some(let error)):
+ self = .failure(error)
+ case (.some(let value), _):
+ self = .success(value)
+ default:
+ self = .failure(ResultError.invalidResultInput)
+ }
+ }
+
+}
+
+public extension Result where Success: Decodable, Failure == Error {
+
+ init(rawValue: [String: Any]?, error: Failure?) {
+ switch (rawValue, error) {
+ case (.none, .some(let error)):
+ self = .failure(error)
+ case (.some(let rawValue), .none):
+ do {
+ let data = try JSONSerialization.data(withJSONObject: rawValue, options: [])
+ let decoder = JSONDecoder()
+ let result = try decoder.decode(Success.self, from: data)
+ self = .success(result)
+ } catch let error {
+ self = .failure(error)
+ }
+ default:
+ self = .failure(ResultError.invalidResultInput)
+ }
+ }
+
+}
+
+public enum ResultError: Error {
+ case invalidResultInput
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListConnector.swift b/Sources/InstantSearchCore/FacetList/FacetListConnector.swift
new file mode 100644
index 00000000..d3fb1aaf
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListConnector.swift
@@ -0,0 +1,120 @@
+//
+// FacetListConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 26/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FacetListConnector: Connection {
+
+ public enum Searcher {
+ case singleIndex(SingleIndexSearcher)
+ case facet(FacetSearcher)
+ }
+
+ public let filterState: FilterState
+ public let searcher: Searcher
+ public let interactor: FacetListInteractor
+ public let attribute: Attribute
+
+ public let filterStateConnection: Connection
+ public let searcherConnection: Connection
+
+ internal init(searcher: Searcher,
+ filterState: FilterState,
+ interactor: FacetListInteractor = .init(),
+ attribute: Attribute,
+ operator: RefinementOperator,
+ groupName: String? = nil) {
+
+ self.filterState = filterState
+ self.searcher = searcher
+ self.interactor = interactor
+ self.attribute = attribute
+
+ self.filterStateConnection = interactor.connectFilterState(filterState,
+ with: attribute,
+ operator: `operator`,
+ groupName: groupName)
+ switch searcher {
+ case .facet(let facetSearcher):
+ searcherConnection = interactor.connectFacetSearcher(facetSearcher)
+
+ case .singleIndex(let singleIndexSearcher):
+ searcherConnection = interactor.connectSearcher(singleIndexSearcher, with: attribute)
+ }
+
+ }
+
+ public convenience init(searcher: SingleIndexSearcher,
+ filterState: FilterState,
+ attribute: Attribute,
+ operator: RefinementOperator,
+ groupName: String? = nil,
+ interactor: FacetListInteractor = .init()) {
+ self.init(searcher: .singleIndex(searcher),
+ filterState: filterState,
+ interactor: interactor,
+ attribute: attribute,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+ public convenience init(searcher: FacetSearcher,
+ filterState: FilterState,
+ attribute: Attribute,
+ operator: RefinementOperator,
+ groupName: String? = nil,
+ interactor: FacetListInteractor = .init()) {
+ self.init(searcher: .facet(searcher),
+ filterState: filterState,
+ interactor: interactor,
+ attribute: attribute,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+ public convenience init(searcher: SingleIndexSearcher,
+ filterState: FilterState,
+ attribute: Attribute,
+ facets: [Facet],
+ selectionMode: SelectionMode,
+ operator: RefinementOperator,
+ groupName: String? = nil) {
+ self.init(searcher: .singleIndex(searcher),
+ filterState: filterState,
+ interactor: .init(facets: facets, selectionMode: selectionMode),
+ attribute: attribute,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+ public convenience init(searcher: FacetSearcher,
+ filterState: FilterState,
+ attribute: Attribute,
+ facets: [Facet],
+ selectionMode: SelectionMode,
+ operator: RefinementOperator,
+ groupName: String? = nil) {
+ self.init(searcher: .facet(searcher),
+ filterState: filterState,
+ interactor: .init(facets: facets, selectionMode: selectionMode),
+ attribute: attribute,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+ public func connect() {
+ filterStateConnection.connect()
+ searcherConnection.connect()
+ }
+
+ public func disconnect() {
+ filterStateConnection.disconnect()
+ searcherConnection.disconnect()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListController.swift b/Sources/InstantSearchCore/FacetList/FacetListController.swift
new file mode 100644
index 00000000..5c050632
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListController.swift
@@ -0,0 +1,11 @@
+//
+// FacetListController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol FacetListController: SelectableListController where Item == Facet {}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+Controller.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+Controller.swift
new file mode 100644
index 00000000..dcd84019
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+Controller.swift
@@ -0,0 +1,90 @@
+//
+// FacetListInteractor+Controller.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FacetList {
+
+ public struct ControllerConnection
: Connection {
+
+ public let facetListInteractor: FacetListInteractor
+ public let controller: Controller
+ public let presenter: SelectableListPresentable?
+ public let externalReload: Bool
+
+ public init(facetListInteractor: FacetListInteractor,
+ controller: Controller,
+ presenter: SelectableListPresentable? = nil,
+ externalReload: Bool = false) {
+ self.facetListInteractor = facetListInteractor
+ self.controller = controller
+ self.presenter = presenter
+ self.externalReload = externalReload
+ }
+
+ public func connect() {
+
+ ControllerConnection.setControllerItemsWith(facets: facetListInteractor.items, selections: facetListInteractor.selections, controller: controller, presenter: presenter)
+
+ controller.onClick = { [weak facetListInteractor] facet in
+ facetListInteractor?.computeSelections(selectingItemForKey: facet.value)
+ }
+
+ facetListInteractor.onItemsChanged.subscribePast(with: controller) { [weak interactor = self.facetListInteractor, presenter = self.presenter] controller, facets in
+ guard let interactor = interactor else { return }
+ ControllerConnection.setControllerItemsWith(facets: facets, selections: interactor.selections, controller: controller, presenter: presenter)
+ }
+
+ facetListInteractor.onSelectionsChanged.subscribePast(with: controller) { [weak interactor = self.facetListInteractor, presenter = self.presenter] controller, selections in
+ guard let interactor = interactor else { return }
+ ControllerConnection.setControllerItemsWith(facets: interactor.items, selections: selections, controller: controller, presenter: presenter)
+ }
+
+ }
+
+ public func disconnect() {
+ controller.onClick = nil
+ facetListInteractor.onItemsChanged.cancelSubscription(for: controller)
+ facetListInteractor.onSelectionsChanged.cancelSubscription(for: controller)
+ }
+
+ /// Add missing refinements with a count of 0 to all returned facets
+ /// Example: if in result we have color: [(red, 10), (green, 5)] and that in the refinements
+ /// we have "color: red" and "color: yellow", the final output would be [(red, 10), (green, 5), (yellow, 0)]
+ private static func merge(_ facets: [Facet], withSelectedValues selections: Set) -> [SelectableItem] {
+ return facets.map { SelectableItem($0, selections.contains($0.value)) }
+ }
+
+ private static func setControllerItemsWith(facets: [Facet], selections: Set, controller: Controller, presenter: SelectableListPresentable?) {
+ let updatedFacets = merge(facets, withSelectedValues: selections)
+ let sortedFacetValues = presenter?.transform(refinementFacets: updatedFacets) ?? updatedFacets
+ controller.setSelectableItems(selectableItems: sortedFacetValues)
+ DispatchQueue.main.async { [weak controller] in
+ controller?.reload()
+ }
+ }
+
+ }
+
+}
+
+public extension FacetListInteractor {
+
+ @discardableResult func connectController(_ controller: C,
+ with presenter: SelectableListPresentable? = nil,
+ externalReload: Bool = false) -> FacetList.ControllerConnection {
+
+ let connection = FacetList.ControllerConnection(facetListInteractor: self,
+ controller: controller,
+ presenter: presenter,
+ externalReload: externalReload)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+FacetSearcher.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FacetSearcher.swift
new file mode 100644
index 00000000..82b7631b
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FacetSearcher.swift
@@ -0,0 +1,54 @@
+//
+// FacetListInteractor+FacetSearcher.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 29/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension FacetListInteractor {
+
+ struct FacetSearcherConnection: Connection {
+
+ public let interactor: FacetListInteractor
+ public let searcher: FacetSearcher
+
+ public func connect() {
+
+ // When new facet search results then update items
+
+ searcher.onResults.subscribePast(with: interactor) { interactor, facetResults in
+ interactor.update(facetResults)
+ }
+
+ // For the case of SFFV, very possible that we forgot to add the
+ // attribute as searchable in `attributesForFaceting`.
+
+ searcher.onError.subscribe(with: interactor) { _, error in
+ if let error = error.1 as? HTTPError, error.statusCode == 400 {
+ assertionFailure(error.message?.description ?? "")
+ }
+ }
+
+ }
+
+ public func disconnect() {
+ searcher.onResults.cancelSubscription(for: interactor)
+ searcher.onError.cancelSubscription(for: interactor)
+ }
+
+ }
+
+}
+
+public extension FacetListInteractor {
+
+ @discardableResult func connectFacetSearcher(_ facetSearcher: FacetSearcher) -> FacetSearcherConnection {
+ let connection = FacetSearcherConnection(interactor: self, searcher: facetSearcher)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+FilterState.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FilterState.swift
new file mode 100644
index 00000000..24847e9e
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FilterState.swift
@@ -0,0 +1,100 @@
+//
+// FacetListInteractor+FilterState.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public enum FacetList {
+
+ public struct FilterStateConnection: Connection {
+
+ public let interactor: FacetListInteractor
+ public let filterState: FilterState
+ public let attribute: Attribute
+ public let `operator`: RefinementOperator
+ public let groupName: String?
+
+ public func connect() {
+ let groupName = self.groupName ?? attribute.rawValue
+ let groupID: FilterGroup.ID
+ switch `operator` {
+ case .and:
+ groupID = .and(name: groupName)
+ case .or:
+ groupID = .or(name: groupName, filterType: .facet)
+ }
+ connect(filterState, to: interactor, with: attribute, via: groupID)
+ }
+
+ public func disconnect() {
+ interactor.onSelectionsComputed.cancelSubscription(for: filterState)
+ filterState.onChange.cancelSubscription(for: interactor)
+ }
+
+ private func connect(_ filterState: FilterState,
+ to interactor: FacetListInteractor,
+ with attribute: Attribute,
+ via groupID: FilterGroup.ID) {
+ whenSelectionsComputedThenUpdateFilterState(interactor: interactor, filterState: filterState, attribute: attribute, via: groupID)
+ whenFilterStateChangedThenUpdateSelections(interactor: interactor, filterState: filterState, via: groupID)
+ }
+
+ private func whenSelectionsComputedThenUpdateFilterState(interactor: FacetListInteractor,
+ filterState: FilterState,
+ attribute: Attribute,
+ via groupID: FilterGroup.ID) {
+ interactor.onSelectionsComputed.subscribePast(with: filterState) { filterState, selections in
+ let filters = selections.map { Filter.Facet(attribute: attribute, stringValue: $0) }
+ filterState.removeAll(fromGroupWithID: groupID)
+ filterState.addAll(filters: filters, toGroupWithID: groupID)
+ filterState.notifyChange()
+ }
+
+ }
+
+ private func whenFilterStateChangedThenUpdateSelections(interactor: FacetListInteractor,
+ filterState: FilterState,
+ via groupID: FilterGroup.ID) {
+
+ func extractString(from filter: Filter.Facet) -> String? {
+ if case .string(let stringValue) = filter.value {
+ return stringValue
+ } else {
+ return nil
+ }
+ }
+
+ filterState.onChange.subscribePast(with: interactor) { interactor, filterState in
+ let filters: [Filter.Facet]
+ switch groupID {
+ case .and(name: let groupName):
+ filters = filterState[and: groupName].filters()
+ case .or(name: let groupName, _):
+ filters = filterState[or: groupName].filters()
+ case .hierarchical:
+ return
+ }
+ interactor.selections = Set(filters.compactMap(extractString))
+ }
+ }
+
+ }
+
+}
+
+public extension FacetListInteractor {
+
+ @discardableResult func connectFilterState(_ filterState: FilterState,
+ with attribute: Attribute,
+ operator: RefinementOperator,
+ groupName: String? = nil) -> FacetList.FilterStateConnection {
+ let connection = FacetList.FilterStateConnection(interactor: self, filterState: filterState, attribute: attribute, operator: `operator`, groupName: groupName)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+SingleIndexSearcher.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+SingleIndexSearcher.swift
new file mode 100644
index 00000000..f4354c9d
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+SingleIndexSearcher.swift
@@ -0,0 +1,47 @@
+//
+// FacetListInteractor+Searcher.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+import AlgoliaSearchClient
+public extension FacetListInteractor {
+
+ struct SingleIndexSearcherConnection: Connection {
+
+ public let facetListInteractor: FacetListInteractor
+ public let searcher: SingleIndexSearcher
+ public let attribute: Attribute
+
+ public func connect() {
+
+ // When new search results then update items
+
+ searcher.onResults.subscribePast(with: facetListInteractor) { [attribute] interactor, searchResults in
+ interactor.items = searchResults.disjunctiveFacets?[attribute] ?? searchResults.facets?[attribute] ?? []
+ }
+
+ searcher.indexQueryState.query.updateQueryFacets(with: attribute)
+
+ }
+
+ public func disconnect() {
+ searcher.onResults.cancelSubscription(for: facetListInteractor)
+ }
+
+ }
+
+}
+
+public extension FacetListInteractor {
+
+ @discardableResult func connectSearcher(_ searcher: SingleIndexSearcher, with attribute: Attribute) -> SingleIndexSearcherConnection {
+ let connection = SingleIndexSearcherConnection(facetListInteractor: self, searcher: searcher, attribute: attribute)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor.swift
new file mode 100644
index 00000000..10bf7598
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor.swift
@@ -0,0 +1,65 @@
+//
+// RefinementFacetInteractor.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 19/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+import AlgoliaSearchClient
+
+public class FacetListInteractor: SelectableListInteractor {
+
+ public let onResultsUpdated: Observer
+ private let mutationQueue: OperationQueue
+
+ public init(facets: [Facet] = [], selectionMode: SelectionMode = .multiple) {
+ self.onResultsUpdated = .init()
+ self.mutationQueue = .init()
+ super.init(items: facets, selectionMode: selectionMode)
+ self.mutationQueue.maxConcurrentOperationCount = 1
+ self.mutationQueue.qualityOfService = .userInitiated
+ }
+
+}
+
+extension FacetListInteractor: ResultUpdatable {
+
+ @discardableResult public func update(_ facetResults: FacetSearchResponse) -> Operation {
+
+ let updateOperation = BlockOperation { [weak self] in
+ self?.items = facetResults.facetHits
+ self?.onResultsUpdated.fire(facetResults)
+ }
+
+ mutationQueue.addOperation(updateOperation)
+
+ return updateOperation
+
+ }
+
+}
+
+public enum FacetSortCriterion {
+
+ case count(order: Order)
+ case alphabetical(order: Order)
+ case isRefined
+
+ public enum Order {
+ case ascending
+ case descending
+ }
+}
+
+public enum RefinementOperator {
+ // when operator is 'and' + one single value can be selected,
+ // we want to keep the other values visible, so we have to do a disjunctive facet
+ // In the case of multi value that can be selected in conjunctive case,
+ // then we avoid doing a disjunctive facet and just do normal conjusctive facet
+ // and only the remaining possible facets will appear.
+ case and
+ case or
+
+}
diff --git a/Sources/InstantSearchCore/FacetList/FacetListPresenter.swift b/Sources/InstantSearchCore/FacetList/FacetListPresenter.swift
new file mode 100644
index 00000000..5e564669
--- /dev/null
+++ b/Sources/InstantSearchCore/FacetList/FacetListPresenter.swift
@@ -0,0 +1,87 @@
+//
+// FacetListPresenter.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 18/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias SelectableItem = (item: T, isSelected: Bool)
+
+public protocol SelectableListPresentable {
+
+ func transform(refinementFacets: [SelectableItem]) -> [SelectableItem]
+}
+
+/// Takes care of building the content of a refinement list given the following:
+/// - The list of Facets + Associated Count
+/// - The list of Facets that have been refined
+/// - Layout settings such as sortBy
+public class FacetListPresenter: SelectableListPresentable {
+
+ let sortBy: [FacetSortCriterion]
+ let limit: Int
+ let showEmptyFacets: Bool
+
+ public init(sortBy: [FacetSortCriterion] = [.count(order: .descending)],
+ limit: Int = .max,
+ showEmptyFacets: Bool = true) {
+ self.sortBy = sortBy
+ self.limit = limit
+ self.showEmptyFacets = showEmptyFacets
+ }
+
+ /// Builds the final list to be displayed in the refinement list
+ public func transform(refinementFacets: [SelectableItem]) -> [SelectableItem] {
+ let filteredOutput = refinementFacets.filter { showEmptyFacets ? true : !$0.item.isEmpty }
+ let sortedOutput = filteredOutput.sorted(by: sorter(using: sortBy))
+ let upperBoundIndex = min(limit, sortedOutput.count)
+ let boundedOutput = sortedOutput[.. (SelectableItem, SelectableItem) -> (Bool) {
+ return { (lhs, rhs) in
+
+ let lhsChecked: Bool = lhs.isSelected
+ let rhsChecked: Bool = rhs.isSelected
+
+ let leftCount = lhs.item.count
+ let rightCount = rhs.item.count
+ let leftValueLowercased = lhs.item.value.lowercased()
+ let rightValueLowercased = rhs.item.value.lowercased()
+
+ // tiebreaking algorithm to do determine the sorting.
+ for sortCriterion in sortCriterions {
+
+ switch sortCriterion {
+ case .isRefined where lhsChecked != rhsChecked:
+ return lhsChecked
+
+ case .count(order: .descending) where leftCount != rightCount:
+ return leftCount > rightCount
+
+ case .count(order: .ascending) where leftCount != rightCount:
+ return leftCount < rightCount
+
+ case .alphabetical(order: .descending) where leftValueLowercased != rightValueLowercased:
+ return leftValueLowercased > rightValueLowercased
+
+ // Sort by Name ascending. Else, Biggest Count wins by default
+ case .alphabetical(order: .ascending) where leftValueLowercased != rightValueLowercased:
+ return leftValueLowercased < rightValueLowercased
+
+ default:
+ break
+ }
+
+ }
+
+ return true
+
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearConnector.swift b/Sources/InstantSearchCore/FilterClear/FilterClearConnector.swift
new file mode 100644
index 00000000..d71c1730
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterClear/FilterClearConnector.swift
@@ -0,0 +1,36 @@
+//
+// FilterClearConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 26/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FilterClearConnector: Connection {
+
+ public let filterState: FilterState
+ public let interactor: FilterClearInteractor
+ public let filterStateConnection: Connection
+
+ init(filterState: FilterState,
+ interactor: FilterClearInteractor = .init(),
+ clearMode: ClearMode = .specified,
+ filterGroupIDs: [FilterGroup.ID]? = nil) {
+ self.filterState = filterState
+ self.interactor = interactor
+ self.filterStateConnection = interactor.connectFilterState(filterState,
+ filterGroupIDs: filterGroupIDs,
+ clearMode: clearMode)
+ }
+
+ public func connect() {
+ filterStateConnection.connect()
+ }
+
+ public func disconnect() {
+ filterStateConnection.disconnect()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearController.swift b/Sources/InstantSearchCore/FilterClear/FilterClearController.swift
new file mode 100644
index 00000000..78de6612
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterClear/FilterClearController.swift
@@ -0,0 +1,15 @@
+//
+// ClearRefinementsController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 24/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol FilterClearController: class {
+
+ var onClick: (() -> Void)? { get set }
+
+}
diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterClearController.swift b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterClearController.swift
new file mode 100644
index 00000000..5a6f0a57
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterClearController.swift
@@ -0,0 +1,39 @@
+//
+// FilterClearInteractor+FilterClearController.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 13/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension FilterClearInteractor {
+
+ struct ControllerConnection: Connection {
+
+ public let interactor: FilterClearInteractor
+ public let controller: FilterClearController
+
+ public func connect() {
+ controller.onClick = { [weak interactor] in
+ interactor?.onTriggered.fire(())
+ }
+ }
+
+ public func disconnect() {
+ controller.onClick = .none
+ }
+ }
+
+}
+
+public extension FilterClearInteractor {
+
+ @discardableResult func connectController(_ controller: FilterClearController) -> ControllerConnection {
+ let connection = ControllerConnection(interactor: self, controller: controller)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterState.swift b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterState.swift
new file mode 100644
index 00000000..ce713a64
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterState.swift
@@ -0,0 +1,78 @@
+//
+// FilterClearInteractor+FilterState.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 24/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension FilterClearInteractor {
+
+ struct FilterStateConnection: Connection {
+
+ public let filterClearInteractor: FilterClearInteractor
+ public let filterState: FilterState
+ public let filterGroupIDs: [FilterGroup.ID]?
+ public let clearMode: ClearMode
+
+ public init(filterClearInteractor: FilterClearInteractor,
+ filterState: FilterState,
+ filterGroupIDs: [FilterGroup.ID]? = nil,
+ clearMode: ClearMode = .specified) {
+ self.filterClearInteractor = filterClearInteractor
+ self.filterState = filterState
+ self.filterGroupIDs = filterGroupIDs
+ self.clearMode = clearMode
+ }
+
+ public func connect() {
+ let filterGroupIDs = self.filterGroupIDs
+ let clearMode = self.clearMode
+
+ filterClearInteractor.onTriggered.subscribe(with: filterState) { filterState, _ in
+ defer {
+ filterState.notifyChange()
+ }
+
+ guard let filterGroupIDs = filterGroupIDs else {
+ filterState.filters.removeAll()
+ return
+ }
+
+ switch clearMode {
+ case .specified:
+ filterState.filters.removeAll(fromGroupWithIDs: filterGroupIDs)
+ case .except:
+ filterState.filters.removeAllExcept(filterGroupIDs)
+ }
+
+ }
+
+ }
+
+ public func disconnect() {
+ filterClearInteractor.onTriggered.cancelSubscription(for: filterState)
+ }
+
+ }
+
+}
+
+public extension FilterClearInteractor {
+
+ @discardableResult func connectFilterState(_ filterState: FilterState,
+ filterGroupIDs: [FilterGroup.ID]? = nil,
+ clearMode: ClearMode = .specified) -> FilterStateConnection {
+ let connection = FilterStateConnection(filterClearInteractor: self, filterState: filterState, filterGroupIDs: filterGroupIDs, clearMode: clearMode)
+ connection.connect()
+ return connection
+ }
+
+}
+
+public enum ClearMode {
+ case specified
+ case except
+}
diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearInteractor.swift b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor.swift
new file mode 100644
index 00000000..311013ae
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor.swift
@@ -0,0 +1,17 @@
+//
+// FilterClearInteractor.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 13/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FilterClearInteractor: EventInteractor {
+ public let onTriggered: Observer
+
+ public init() {
+ onTriggered = .init()
+ }
+}
diff --git a/Sources/InstantSearchCore/FilterList/FacetFilterListConnector.swift b/Sources/InstantSearchCore/FilterList/FacetFilterListConnector.swift
new file mode 100644
index 00000000..f9064162
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FacetFilterListConnector.swift
@@ -0,0 +1,32 @@
+//
+// FacetFilterListConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 26/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias FacetFilter = Filter.Facet
+public typealias NumericFilter = Filter.Numeric
+public typealias TagFilter = Filter.Tag
+
+public typealias FacetFilterListConnector = FilterListConnector
+
+public extension FacetFilterListConnector where Filter == FacetFilter {
+
+ convenience init(filters: [Filter] = [],
+ selectionMode: SelectionMode = .multiple,
+ filterState: FilterState,
+ `operator`: RefinementOperator,
+ groupName: String) {
+ let interactor = FacetFilterListInteractor(items: filters,
+ selectionMode: selectionMode)
+ self.init(filterState: filterState,
+ interactor: interactor,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListConnector.swift b/Sources/InstantSearchCore/FilterList/FilterListConnector.swift
new file mode 100644
index 00000000..8cffaadb
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListConnector.swift
@@ -0,0 +1,36 @@
+//
+// FilterListConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 26/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FilterListConnector: Connection {
+
+ public let filterState: FilterState
+ public let interactor: FilterListInteractor
+ public let connectionFilterState: Connection
+
+ public init(filterState: FilterState,
+ interactor: FilterListInteractor,
+ `operator`: RefinementOperator,
+ groupName: String) {
+ self.filterState = filterState
+ self.interactor = interactor
+ self.connectionFilterState = interactor.connectFilterState(filterState,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+ public func connect() {
+ connectionFilterState.connect()
+ }
+
+ public func disconnect() {
+ connectionFilterState.disconnect()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListController.swift b/Sources/InstantSearchCore/FilterList/FilterListController.swift
new file mode 100644
index 00000000..73d03fc4
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListController.swift
@@ -0,0 +1,15 @@
+//
+// FilterListController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol FilterListController: SelectableListController where Item: FilterType {}
+
+public protocol FacetFilterListController: FilterListController where Item == Filter.Facet {}
+public protocol NumericFilterListController: FilterListController where Item == Filter.Numeric {}
+public protocol TagFilterListController: FilterListController where Item == Filter.Tag {}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Controller.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Controller.swift
new file mode 100644
index 00000000..c8af2528
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Controller.swift
@@ -0,0 +1,57 @@
+//
+// FilterListInteractor+Controller.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FilterList {
+
+ public struct ControllerConnection: Connection where Controller.Item == Filter {
+
+ public let interactor: FilterListInteractor
+ public let controller: Controller
+
+ public func connect() {
+
+ func setControllerItemsWith(items: [Filter], selections: Set) {
+ let selectableItems = items.map { ($0, selections.contains($0)) }
+ controller.setSelectableItems(selectableItems: selectableItems)
+ controller.reload()
+ }
+
+ setControllerItemsWith(items: interactor.items, selections: interactor.selections)
+
+ controller.onClick = interactor.computeSelections(selectingItemForKey:)
+
+ interactor.onItemsChanged.subscribePast(with: controller) { [weak interactor] _, items in
+ setControllerItemsWith(items: items, selections: interactor!.selections)
+ }.onQueue(.main)
+
+ interactor.onSelectionsChanged.subscribePast(with: controller) { [weak interactor] _, selections in
+ setControllerItemsWith(items: interactor!.items, selections: selections)
+ }.onQueue(.main)
+
+ }
+
+ public func disconnect() {
+ interactor.onItemsChanged.cancelSubscription(for: controller)
+ interactor.onSelectionsChanged.cancelSubscription(for: controller)
+ }
+
+ }
+
+}
+
+public extension SelectableListInteractor where Key == Item, Item: FilterType {
+
+ @discardableResult func connectController(_ controller: Controller) -> FilterList.ControllerConnection {
+ let connection = FilterList.ControllerConnection(interactor: self, controller: controller)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Facet.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Facet.swift
new file mode 100644
index 00000000..728709a7
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Facet.swift
@@ -0,0 +1,19 @@
+//
+// FilterListInteractor+Facet.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias FacetFilterListInteractor = FilterListInteractor
+
+public extension FacetFilterListInteractor {
+
+ convenience init(facetFilters: [Filter.Facet] = []) {
+ self.init(items: facetFilters, selectionMode: .multiple)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+FilterState.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+FilterState.swift
new file mode 100644
index 00000000..682f8392
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+FilterState.swift
@@ -0,0 +1,94 @@
+//
+// FilterListInteractor+FilterState.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public enum FilterList {
+
+ public struct FilterStateConnection: Connection {
+
+ public let interactor: SelectableListInteractor
+ public let filterState: FilterState
+ public let `operator`: RefinementOperator
+ public let groupName: String
+
+ public init(interactor: SelectableListInteractor,
+ filterState: FilterState,
+ `operator`: RefinementOperator,
+ groupName: String = "") {
+ self.interactor = interactor
+ self.filterState = filterState
+ self.operator = `operator`
+ self.groupName = groupName
+ }
+
+ public func connect() {
+ switch `operator` {
+ case .or:
+ connectFilterState(filterState, to: interactor, via: filterState[or: groupName])
+ case .and:
+ connectFilterState(filterState, to: interactor, via: SpecializedAndGroupAccessor(filterState[and: groupName]))
+ }
+ }
+
+ public func disconnect() {
+ interactor.onSelectionsComputed.cancelSubscription(for: filterState)
+ filterState.onChange.cancelSubscription(for: interactor)
+ }
+
+ private func connectFilterState, Accessor: SpecializedGroupAccessor>(_ filterState: FilterState,
+ to interactor: Interactor, via accessor: Accessor) where Accessor.Filter == Filter {
+ whenSelectionsComputedThenUpdateFilterState(interactor: interactor, filterState: filterState, via: accessor)
+ whenFilterStateChangedThenUpdateSelections(interactor: interactor, filterState: filterState, via: accessor)
+ }
+
+ private func whenSelectionsComputedThenUpdateFilterState, Accessor: SpecializedGroupAccessor>(interactor: Interactor,
+ filterState: FilterState,
+
+ via accessor: Accessor) where Accessor.Filter == Filter {
+
+ interactor.onSelectionsComputed.subscribePast(with: filterState) { [weak interactor] filterState, filters in
+
+ guard let strongInteractor = interactor else { return }
+ switch strongInteractor.selectionMode {
+ case .multiple:
+ accessor.removeAll()
+
+ case .single:
+ accessor.removeAll(strongInteractor.items)
+ }
+
+ accessor.addAll(filters)
+
+ filterState.notifyChange()
+ }
+
+ }
+
+ private func whenFilterStateChangedThenUpdateSelections, Accessor: SpecializedGroupAccessor>(interactor: Interactor, filterState: FilterState,
+ via accessor: Accessor) where Accessor.Filter == Filter {
+ filterState.onChange.subscribePast(with: interactor) { interactor, _ in
+ interactor.selections = Set(accessor.filters())
+ }
+ }
+
+ }
+
+}
+
+public extension SelectableListInteractor where Key == Item, Item: FilterType {
+
+ @discardableResult func connectFilterState(_ filterState: FilterState,
+ operator: RefinementOperator,
+ groupName: String = "") -> FilterList.FilterStateConnection {
+ let connection = FilterList.FilterStateConnection(interactor: self, filterState: filterState, operator: `operator`, groupName: groupName)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Numeric.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Numeric.swift
new file mode 100644
index 00000000..45b2371c
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Numeric.swift
@@ -0,0 +1,19 @@
+//
+// FilterListInteractor+Numeric.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias NumericFilterListInteractor = FilterListInteractor
+
+public extension NumericFilterListInteractor {
+
+ convenience init(numericFilters: [Filter.Numeric] = []) {
+ self.init(items: numericFilters, selectionMode: .single)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Tag.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Tag.swift
new file mode 100644
index 00000000..99c6bb77
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Tag.swift
@@ -0,0 +1,19 @@
+//
+// FilterListInteractor+Tag.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias TagFilterListInteractor = FilterListInteractor
+
+public extension TagFilterListInteractor {
+
+ convenience init(tagFilters: [Filter.Tag] = []) {
+ self.init(items: tagFilters, selectionMode: .multiple)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor.swift
new file mode 100644
index 00000000..284329c4
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor.swift
@@ -0,0 +1,11 @@
+//
+// SelectableListInteractor+Filter.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 17/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias FilterListInteractor = SelectableListInteractor
diff --git a/Sources/InstantSearchCore/FilterList/NumericFilterListConnector.swift b/Sources/InstantSearchCore/FilterList/NumericFilterListConnector.swift
new file mode 100644
index 00000000..34bce9a4
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/NumericFilterListConnector.swift
@@ -0,0 +1,28 @@
+//
+// NumericFilterListConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 26/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias NumericFilterListConnector = FilterListConnector
+
+public extension NumericFilterListConnector {
+
+ convenience init(numericFilters: [NumericFilter] = [],
+ selectionMode: SelectionMode = .single,
+ filterState: FilterState,
+ `operator`: RefinementOperator,
+ groupName: String) {
+ let interactor = NumericFilterListInteractor(items: numericFilters,
+ selectionMode: selectionMode)
+ self.init(filterState: filterState,
+ interactor: interactor,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterList/TagFilterListConnector.swift b/Sources/InstantSearchCore/FilterList/TagFilterListConnector.swift
new file mode 100644
index 00000000..26755da2
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterList/TagFilterListConnector.swift
@@ -0,0 +1,28 @@
+//
+// TagFilterListConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 26/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias TagFilterListConnector = FilterListConnector
+
+public extension TagFilterListConnector {
+
+ convenience init(tagFilters: [TagFilter] = [],
+ selectionMode: SelectionMode = .multiple,
+ filterState: FilterState,
+ `operator`: RefinementOperator,
+ groupName: String) {
+ let interactor = TagFilterListInteractor(items: tagFilters,
+ selectionMode: selectionMode)
+ self.init(filterState: filterState,
+ interactor: interactor,
+ operator: `operator`,
+ groupName: groupName)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/Accessors/AndGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/AndGroupAccessor.swift
new file mode 100644
index 00000000..882f2d99
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/Accessors/AndGroupAccessor.swift
@@ -0,0 +1,79 @@
+//
+// AndGroupAccessor.swift
+// AlgoliaSearch OSX
+//
+// Created by Vladislav Fitc on 24/12/2018.
+// Copyright © 2018 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/// Provides a convenient interface to a conjunctive group contained by FilterState
+public struct AndGroupAccessor: GroupAccessor {
+
+ let filtersContainer: FiltersContainer
+ let groupID: FilterGroup.ID
+
+ /// A Boolean value indicating whether group contains at least on filter
+ public var isEmpty: Bool {
+ return filtersContainer.filters.getFilters(forGroupWithID: groupID).isEmpty
+ }
+
+ init(filtersContainer: FiltersContainer, groupName: String) {
+ self.filtersContainer = filtersContainer
+ self.groupID = .and(name: groupName)
+ }
+
+ /// Adds filter to group
+ /// - parameter filter: filter to add
+ public func add(_ filters: FilterType...) {
+ filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID)
+ }
+
+ /// Adds the filters of a sequence to group
+ /// - parameter filters: sequence of filters to add
+ public func addAll(_ filters: [FilterType]) {
+ filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID)
+ }
+
+ /// Tests whether group contains a filter
+ /// - parameter filter: sought filter
+ public func contains(_ filter: T) -> Bool {
+ return filtersContainer.filters.contains(filter, inGroupWithID: groupID)
+ }
+
+ /// Removes all filters with specified attribute from group
+ /// - parameter attribute: specified attribute
+ public func removeAll(for attribute: Attribute) {
+ return filtersContainer.filters.removeAll(for: attribute, fromGroupWithID: groupID)
+ }
+
+ /// Removes filter from group
+ /// - parameter filter: filter to remove
+ @discardableResult public func remove(_ filter: T) -> Bool {
+ return filtersContainer.filters.remove(filter, fromGroupWithID: groupID)
+ }
+
+ /// Removes a sequence of filters from group
+ /// - parameter filters: sequence of filters to remove
+ @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == FilterType {
+ return filtersContainer.filters.removeAll(filters, fromGroupWithID: groupID)
+ }
+
+ /// Removes all filters in group
+ public func removeAll() {
+ filtersContainer.filters.removeAll(fromGroupWithID: groupID)
+ }
+
+ /// Removes all filters in other all groups
+ public func removeAllOthers() {
+ filtersContainer.filters.removeAllExcept([groupID])
+ }
+
+ /// Removes filter from group if contained by it, otherwise adds filter to group
+ /// - parameter filter: filter to toggle
+ public func toggle(_ filter: T) {
+ filtersContainer.filters.toggle(filter, inGroupWithID: groupID)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/Accessors/GroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/GroupAccessor.swift
new file mode 100644
index 00000000..c4ad708f
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/Accessors/GroupAccessor.swift
@@ -0,0 +1,128 @@
+//
+// GroupAccessor.swift
+// AlgoliaSearch OSX
+//
+// Created by Vladislav Fitc on 24/12/2018.
+// Copyright © 2018 Algolia. All rights reserved.
+//
+
+import Foundation
+
+protocol FiltersContainer: class {
+ var filters: FilterState.Storage { get set }
+}
+
+extension FiltersContainer {
+
+ public subscript(and groupName: String) -> AndGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+ public subscript(or groupName: String, type: F.Type) -> OrGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+ public subscript(or groupName: String) -> OrGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+ public subscript(hierarchical groupName: String) -> HierarchicalGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+}
+
+public class ReadOnlyFiltersContainer {
+
+ class StorageContainer: FiltersContainer {
+ var filters: FilterState.Storage
+ init(filterState: FilterState) {
+ self.filters = filterState.filters
+ }
+ }
+
+ let filtersContainer: FiltersContainer
+
+ init(filterState: FilterState) {
+ self.filtersContainer = StorageContainer(filterState: filterState)
+ }
+
+ public subscript(and groupName: String) -> ReadOnlyGroupAccessor {
+ return ReadOnlyGroupAccessor(SpecializedAndGroupAccessor(filtersContainer[and: groupName]))
+ }
+
+ public subscript(or groupName: String) -> ReadOnlyGroupAccessor {
+ return ReadOnlyGroupAccessor(filtersContainer[or: groupName])
+ }
+
+ public subscript(hierarchical groupName: String) -> ReadOnlyGroupAccessor {
+ return ReadOnlyGroupAccessor(filtersContainer[hierarchical: groupName])
+ }
+
+}
+
+extension ReadOnlyFiltersContainer: FilterGroupsConvertible {
+ public func toFilterGroups() -> [FilterGroupType] {
+ return filtersContainer.filters.toFilterGroups()
+ }
+
+}
+
+/// Provides a convenient interface to a concrete group contained by FilterState
+public protocol GroupAccessor {
+
+ var isEmpty: Bool { get }
+
+ func removeAll(for attribute: Attribute)
+ func removeAll()
+ func removeAllOthers()
+
+}
+
+public class ReadOnlyGroupAccessor {
+
+ var storedIsEmpty: () -> Bool
+ var storedGetFilters: () -> [Filter]
+ var storedGetFiltersForAttribute: (Attribute) -> [Filter]
+ var storedContains: (Filter) -> Bool
+
+ init(_ accessor: A) where A.Filter == Filter {
+ storedIsEmpty = { accessor.isEmpty }
+ storedGetFilters = { return accessor.filters() }
+ storedGetFiltersForAttribute = { return accessor.filters(for: $0) }
+ storedContains = { return accessor.contains($0) }
+ }
+
+ var isEmpty: Bool {
+ return storedIsEmpty()
+ }
+
+ func filters() -> [Filter] {
+ return storedGetFilters()
+ }
+
+ func filters(for attribute: Attribute) -> [Filter] {
+ return storedGetFiltersForAttribute(attribute)
+ }
+
+ func contains(_ filter: Filter) -> Bool {
+ return storedContains(filter)
+ }
+
+}
+
+public protocol SpecializedGroupAccessor: GroupAccessor {
+
+ associatedtype Filter: FilterType
+
+ func filters() -> [Filter]
+ func filters(for attribute: Attribute) -> [Filter]
+
+ func add(_ filters: Filter...)
+ func addAll(_ filters: S) where S.Element == Filter
+ func contains(_ filter: Filter) -> Bool
+ func remove(_ filter: Filter)
+ @discardableResult func removeAll(_ filters: S) -> Bool where S.Element == Filter
+ func toggle(_ filter: Filter)
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/Accessors/HierarchicalGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/HierarchicalGroupAccessor.swift
new file mode 100644
index 00000000..e5fe05c2
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/Accessors/HierarchicalGroupAccessor.swift
@@ -0,0 +1,105 @@
+//
+// HierarchicalGroupAccessor.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 12/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/// Provides a convenient interface to a hierarchical group contained by FilterState
+public struct HierarchicalGroupAccessor: SpecializedGroupAccessor {
+
+ public typealias Filter = FacetFilter
+
+ let groupID: FilterGroup.ID
+ var filtersContainer: FiltersContainer
+
+ public var isEmpty: Bool {
+ return filtersContainer.filters.isEmpty
+ }
+
+ var hierarchicalAttributes: [Attribute] {
+ return filtersContainer.filters.hierarchicalAttributes(forGroupWithName: groupID.name)
+ }
+
+ var hierarchicalFilters: [Filter] {
+ return filtersContainer.filters.hierarchicalFilters(forGroupWithName: groupID.name)
+ }
+
+ func set(_ hierarchicalAttributes: [Attribute]) {
+ filtersContainer.filters.set(hierarchicalAttributes, forGroupWithName: groupID.name)
+ }
+
+ func set(_ hierarchicalFilters: [Filter]) {
+ filtersContainer.filters.set(hierarchicalFilters, forGroupWithName: groupID.name)
+ }
+
+ init(filtersContainer: FiltersContainer, groupName: String) {
+ self.filtersContainer = filtersContainer
+ self.groupID = .hierarchical(name: groupName)
+ }
+
+ /// Adds filter to group
+ /// - parameter filter: filter to add
+ public func add(_ filters: Filter...) {
+ filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID)
+ }
+
+ /// Adds the filters of a sequence to group
+ /// - parameter filters: sequence of filters to add
+ public func addAll(_ filters: S) where S.Element == Filter {
+ filtersContainer.filters.addAll(filters: filters.map { $0 as FilterType }, toGroupWithID: groupID)
+ }
+
+ /// Tests whether group contains a filter
+ /// - parameter filter: sought filter
+ public func contains(_ filter: Filter) -> Bool {
+ return filtersContainer.filters.contains(filter, inGroupWithID: groupID)
+ }
+
+ /// Removes all filters with specified attribute from group
+ /// - parameter attribute: specified attribute
+ public func removeAll(for attribute: Attribute) {
+ return filtersContainer.filters.removeAll(for: attribute, fromGroupWithID: groupID)
+ }
+
+ /// Removes filter from group
+ /// - parameter filter: filter to remove
+ public func remove(_ filter: Filter) {
+ filtersContainer.filters.remove(filter, fromGroupWithID: groupID)
+ }
+
+ /// Removes a sequence of filters from group
+ /// - parameter filters: sequence of filters to remove
+ @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == Filter {
+ filtersContainer.filters.removeAll(fromGroupWithID: groupID)
+ return false
+ }
+
+ /// Removes all filters in group
+ public func removeAll() {
+ filtersContainer.filters.removeAll(fromGroupWithID: groupID)
+ }
+
+ /// Removes all filters in other all groups
+ public func removeAllOthers() {
+ filtersContainer.filters.removeAllExcept([groupID])
+ }
+
+ /// Removes filter from group if contained by it, otherwise adds filter to group
+ /// - parameter filter: filter to toggle
+ public func toggle(_ filter: Filter) {
+ filtersContainer.filters.toggle(filter, inGroupWithID: groupID)
+ }
+
+ public func filters(for attribute: Attribute) -> [Filter] {
+ return filtersContainer.filters.getFilters(for: attribute).compactMap { $0.filter as? Filter }
+ }
+
+ public func filters() -> [Filter] {
+ return filtersContainer.filters.getFilters().compactMap { $0.filter as? Filter }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/Accessors/OrGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/OrGroupAccessor.swift
new file mode 100644
index 00000000..366ed3d5
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/Accessors/OrGroupAccessor.swift
@@ -0,0 +1,86 @@
+//
+// OrGroupAccessor.swift
+// AlgoliaSearch OSX
+//
+// Created by Vladislav Fitc on 24/12/2018.
+// Copyright © 2018 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/// Provides a convenient interface to a disjunctive group contained by FilterState
+public struct OrGroupAccessor: SpecializedGroupAccessor {
+
+ let filtersContainer: FiltersContainer
+ let groupID: FilterGroup.ID
+
+ /// A Boolean value indicating whether group contains at least on filter
+ public var isEmpty: Bool {
+ return filtersContainer.filters.getFilters(forGroupWithID: groupID).isEmpty
+ }
+
+ init(filtersContainer: FiltersContainer, groupName: String) {
+ self.filtersContainer = filtersContainer
+ let filterType = FilterGroup.ID.Filter(Filter.self)!
+ self.groupID = .or(name: groupName, filterType: filterType)
+ }
+
+ /// Adds filter to group
+ /// - parameter filter: filter to add
+ public func add(_ filters: Filter...) {
+ filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID)
+ }
+
+ /// Adds the filters of a sequence to group
+ /// - parameter filters: sequence of filters to add
+ public func addAll(_ filters: S) where S.Element == Filter {
+ filtersContainer.filters.addAll(filters: filters.map { $0 as FilterType }, toGroupWithID: groupID)
+ }
+
+ /// Tests whether group contains a filter
+ /// - parameter filter: sought filter
+ public func contains(_ filter: Filter) -> Bool {
+ return filtersContainer.filters.contains(filter, inGroupWithID: groupID)
+ }
+
+ /// Removes all filters with specified attribute from group
+ /// - parameter attribute: specified attribute
+ public func removeAll(for attribute: Attribute) {
+ return filtersContainer.filters.removeAll(for: attribute, fromGroupWithID: groupID)
+ }
+
+ public func remove(_ filter: Filter) {
+ _ = filtersContainer.filters.remove(filter, fromGroupWithID: groupID)
+ }
+
+ /// Removes a sequence of filters from group
+ /// - parameter filters: sequence of filters to remove
+ @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == Filter {
+ return filtersContainer.filters.removeAll(filters.map { $0 as FilterType }, fromGroupWithID: groupID)
+ }
+
+ /// Removes all filters in group
+ public func removeAll() {
+ filtersContainer.filters.removeAll(fromGroupWithID: groupID)
+ }
+
+ /// Removes all filters in other all groups
+ public func removeAllOthers() {
+ filtersContainer.filters.removeAllExcept([groupID])
+ }
+
+ /// Removes filter from group if contained by it, otherwise adds filter to group
+ /// - parameter filter: filter to toggleE
+ public func toggle(_ filter: Filter) {
+ filtersContainer.filters.toggle(filter, inGroupWithID: groupID)
+ }
+
+ public func filters(for attribute: Attribute) -> [Filter] {
+ return filtersContainer.filters.getFilters(forGroupWithID: groupID).filter { $0.attribute == attribute }.compactMap { $0.filter as? Filter }
+ }
+
+ public func filters() -> [Filter] {
+ return filtersContainer.filters.getFilters(forGroupWithID: groupID).compactMap { $0.filter as? Filter }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/Accessors/SpecializedAndGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/SpecializedAndGroupAccessor.swift
new file mode 100644
index 00000000..27c2138d
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/Accessors/SpecializedAndGroupAccessor.swift
@@ -0,0 +1,93 @@
+//
+// SpecializedAndGroupAccessor.swift
+// AlgoliaSearch OSX
+//
+// Created by Vladislav Fitc on 26/12/2018.
+// Copyright © 2018 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/// Provides a convenient interface to a conjunctive group contained by FilterState specialized for filters of concrete type
+public struct SpecializedAndGroupAccessor: SpecializedGroupAccessor {
+
+ private var genericAccessor: AndGroupAccessor
+
+ var filtersContainer: FiltersContainer {
+ return genericAccessor.filtersContainer
+ }
+
+ public var groupID: FilterGroup.ID {
+ return genericAccessor.groupID
+ }
+
+ /// A Boolean value indicating whether group contains at least on filter
+ public var isEmpty: Bool {
+ return genericAccessor.isEmpty
+ }
+
+ init(_ genericAccessor: AndGroupAccessor) {
+ self.genericAccessor = genericAccessor
+ }
+
+ /// Adds filter to group
+ /// - parameter filter: filter to add
+ public func add(_ filters: Filter...) {
+ genericAccessor.addAll(filters)
+ }
+
+ /// Adds the filters of a sequence to group
+ /// - parameter filters: sequence of filters to add
+ public func addAll(_ filters: S) where S.Element == Filter {
+ genericAccessor.addAll(filters.map { $0 as FilterType })
+ }
+
+ /// Tests whether group contains a filter
+ /// - parameter filter: sought filter
+ public func contains(_ filter: Filter) -> Bool {
+ return genericAccessor.contains(filter)
+ }
+
+ /// Removes all filters with specified attribute from group
+ /// - parameter attribute: specified attribute
+ public func removeAll(for attribute: Attribute) {
+ return genericAccessor.removeAll(for: attribute)
+ }
+
+ /// Removes filter from group
+ /// - parameter filter: filter to remove
+ public func remove(_ filter: Filter) {
+ genericAccessor.remove(filter)
+ }
+
+ /// Removes a sequence of filters from group
+ /// - parameter filters: sequence of filters to remove
+ @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == Filter {
+ return genericAccessor.removeAll(filters.map { $0 as FilterType })
+ }
+
+ /// Removes all filters in group
+ public func removeAll() {
+ genericAccessor.removeAll()
+ }
+
+ /// Removes all filters in other all groups
+ public func removeAllOthers() {
+ genericAccessor.removeAllOthers()
+ }
+
+ /// Removes filter from group if contained by it, otherwise adds filter to group
+ /// - parameter filter: filter to toggle
+ public func toggle(_ filter: Filter) {
+ genericAccessor.toggle(filter)
+ }
+
+ public func filters(for attribute: Attribute) -> [Filter] {
+ return filtersContainer.filters.getFilters(forGroupWithID: groupID).filter { $0.attribute == attribute }.compactMap { $0.filter as? Filter }
+ }
+
+ public func filters() -> [Filter] {
+ return filtersContainer.filters.getFilters(forGroupWithID: groupID).compactMap { $0.filter as? Filter }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterConverter.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterConverter.swift
new file mode 100644
index 00000000..bd279609
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterConverter.swift
@@ -0,0 +1,13 @@
+//
+// FilterConverter.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FilterConverter {
+ public init() {}
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterGroupConverter.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterGroupConverter.swift
new file mode 100644
index 00000000..7ca1c714
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterGroupConverter.swift
@@ -0,0 +1,13 @@
+//
+// FilterGroupConverter.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FilterGroupConverter {
+ public init() {}
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/LegacySyntax.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/LegacySyntax.swift
new file mode 100644
index 00000000..da31c0ea
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/LegacySyntax.swift
@@ -0,0 +1,127 @@
+//
+// LegacySyntax.swift
+// AlgoliaSearch
+//
+// Created by Vladislav Fitc on 05/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+protocol LegacySyntaxConvertible {
+ var legacyForm: FiltersStorage { get }
+}
+
+extension FilterConverter {
+
+ public func legacy(_ filter: FilterType) -> FiltersStorage? {
+ return (filter as? LegacySyntaxConvertible)?.legacyForm
+ }
+
+}
+
+extension FilterGroupConverter {
+
+ public func legacy(_ group: FilterGroupType) -> FiltersStorage? {
+ return (group as? LegacySyntaxConvertible)?.legacyForm
+ }
+
+ public func legacy(_ groups: C) -> FiltersStorage? where C.Element == FilterGroupType {
+ let units = groups
+ .filter { !$0.filters.isEmpty }
+ .compactMap { $0 as? LegacySyntaxConvertible }
+ .map(\.legacyForm)
+ .flatMap(\.units)
+ return FiltersStorage(units: units)
+ }
+
+}
+
+extension Filter.Numeric: LegacySyntaxConvertible {
+
+ public var legacyForm: FiltersStorage {
+
+ switch value {
+ case .comparison(let `operator`, let value):
+ let `operator` = isNegated ? `operator`.inversion : `operator`
+ let expression = """
+ \(attribute) \(`operator`.rawValue) \(value)
+ """
+ return .and(.and(expression))
+
+ case .range(let range):
+ let units = [
+ Filter.Numeric(attribute: attribute, operator: isNegated ? .lessThan : .greaterThanOrEqual, value: range.lowerBound),
+ Filter.Numeric(attribute: attribute, operator: isNegated ? .greaterThan : .lessThanOrEqual, value: range.upperBound)
+ ]
+ .compactMap { $0.legacyForm }
+ .flatMap(\.units)
+ return FiltersStorage(units: units)
+ }
+
+ }
+
+}
+
+extension Filter.Facet: LegacySyntaxConvertible {
+
+ public var legacyForm: FiltersStorage {
+ let scoreExpression = score.flatMap { "" } ?? ""
+ let valuePrefix = isNegated ? "-" : ""
+ let expression = """
+ \(attribute):\(valuePrefix)\(value)\(scoreExpression)
+ """
+ return .and(.and(expression))
+ }
+
+}
+
+extension Filter.Tag: LegacySyntaxConvertible {
+
+ public var legacyForm: FiltersStorage {
+ let valuePrefix = isNegated ? "-" : ""
+ let expression = """
+ \(attribute):\(valuePrefix)\(value)
+ """
+ return .and(.and(expression))
+ }
+
+}
+
+extension FilterGroup.And: LegacySyntaxConvertible {
+
+ var legacyForm: FiltersStorage {
+ let rawFilters = filters
+ .compactMap { $0 as? LegacySyntaxConvertible }
+ .map(\.legacyForm)
+ .flatMap(\.units)
+ .flatMap(\.rawFilters)
+ return .and(.and(rawFilters))
+ }
+
+}
+
+extension FilterGroup.Or: LegacySyntaxConvertible {
+
+ var legacyForm: FiltersStorage {
+ let rawFilters = filters
+ .compactMap { $0 as? LegacySyntaxConvertible }
+ .map(\.legacyForm)
+ .flatMap(\.units)
+ .flatMap(\.rawFilters)
+ return .and(.or(rawFilters))
+ }
+
+}
+
+internal extension FiltersStorage.Unit {
+
+ var rawFilters: [String] {
+ switch self {
+ case .and(let values),
+ .or(let values):
+ return values
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/SQLSyntax.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/SQLSyntax.swift
new file mode 100644
index 00000000..2c74121b
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/SQLSyntax.swift
@@ -0,0 +1,115 @@
+//
+// SQLSyntax.swift
+// AlgoliaSearch
+//
+// Created by Vladislav Fitc on 05/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+protocol SQLSyntaxConvertible {
+ var sqlForm: String { get }
+}
+
+extension FilterConverter {
+
+ public func sql(_ filter: FilterType) -> String? {
+ return (filter as? SQLSyntaxConvertible)?.sqlForm
+ }
+
+}
+
+extension FilterGroupConverter {
+
+ public func sql(_ group: FilterGroupType) -> String? {
+ return (group as? SQLSyntaxConvertible)?.sqlForm
+ }
+
+ public func sql(_ groupList: C) -> String? where C.Element == FilterGroupType {
+ guard !groupList.isEmpty else { return nil }
+ return groupList
+ .filter { !$0.filters.isEmpty }
+ .compactMap(sql)
+ .joined(separator: " AND ")
+ }
+
+}
+
+extension Filter.Numeric: SQLSyntaxConvertible {
+
+ public var sqlForm: String {
+ let expression: String
+ switch value {
+ case .comparison(let `operator`, let value):
+ expression = """
+ "\(attribute)" \(`operator`.rawValue) \(value)
+ """
+
+ case .range(let range):
+ expression = """
+ "\(attribute)":\(range.lowerBound) TO \(range.upperBound)
+ """
+ }
+ let prefix = isNegated ? "NOT " : ""
+ return prefix + expression
+ }
+
+}
+
+extension Filter.Facet: SQLSyntaxConvertible {
+
+ public var sqlForm: String {
+ let scoreExpression = score.flatMap { "" } ?? ""
+ let expression = """
+ "\(attribute)":"\(value)\(scoreExpression)"
+ """
+ let prefix = isNegated ? "NOT " : ""
+ return prefix + expression
+ }
+
+}
+
+extension Filter.Tag: SQLSyntaxConvertible {
+
+ public var sqlForm: String {
+ let expression = """
+ "\(attribute)":"\(value)"
+ """
+ let prefix = isNegated ? "NOT " : ""
+ return prefix + expression
+ }
+
+}
+
+extension SQLSyntaxConvertible where Self: FilterGroupType {
+
+ func groupSQLForm(for filters: [FilterType], withSeparator separator: String) -> String {
+
+ let compatibleFilters = filters.compactMap { $0 as? SQLSyntaxConvertible }
+
+ if compatibleFilters.isEmpty {
+ return ""
+ } else {
+ return "( \(compatibleFilters.map { $0.sqlForm }.joined(separator: separator)) )"
+ }
+
+ }
+
+}
+
+extension FilterGroup.And: SQLSyntaxConvertible {
+
+ public var sqlForm: String {
+ return groupSQLForm(for: filters, withSeparator: " AND ")
+ }
+
+}
+
+extension FilterGroup.Or: SQLSyntaxConvertible {
+
+ public var sqlForm: String {
+ return groupSQLForm(for: filters, withSeparator: " OR ")
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/FacetFilter.swift b/Sources/InstantSearchCore/FilterState/FIlter/FacetFilter.swift
new file mode 100644
index 00000000..41b8496a
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/FacetFilter.swift
@@ -0,0 +1,134 @@
+//
+// Facet.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/** Defines facet filter
+ # See also:
+ [Filter by string](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-string/)
+ [Filter by boolean](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-boolean/)
+ */
+
+public extension Filter {
+
+ struct Facet: FilterType, Equatable {
+
+ public let attribute: Attribute
+ public let value: ValueType
+ public var isNegated: Bool
+ public let score: Int?
+
+ public init(attribute: Attribute, value: ValueType, isNegated: Bool = false, score: Int? = nil) {
+ self.attribute = attribute
+ self.isNegated = isNegated
+ self.value = value
+ self.score = score
+ }
+
+ public init(attribute: Attribute, stringValue: String, isNegated: Bool = false) {
+ self.init(attribute: attribute, value: .string(stringValue), isNegated: isNegated)
+ }
+
+ public init(attribute: Attribute, floatValue: Float, isNegated: Bool = false) {
+ self.init(attribute: attribute, value: .float(floatValue), isNegated: isNegated)
+ }
+
+ public init(attribute: Attribute, boolValue: Bool, isNegated: Bool = false) {
+ self.init(attribute: attribute, value: .bool(boolValue), isNegated: isNegated)
+ }
+
+ }
+
+}
+
+extension Filter.Facet: Hashable {}
+
+extension Filter.Facet: RawRepresentable {
+
+ public typealias RawValue = (Attribute, ValueType)
+
+ public init?(rawValue: (Attribute, Filter.Facet.ValueType)) {
+ self.init(attribute: rawValue.0, value: rawValue.1)
+ }
+
+ public var rawValue: (Attribute, Filter.Facet.ValueType) {
+ return (attribute, value)
+ }
+
+}
+
+extension Filter.Facet: CustomStringConvertible {
+
+ public var description: String {
+ return "\(attribute): \(value.description)"
+ }
+
+}
+
+extension Filter.Facet {
+
+ public enum ValueType: CustomStringConvertible, Hashable {
+
+ case string(String)
+ case float(Float)
+ case bool(Bool)
+
+ public var description: String {
+ switch self {
+ case .string(let value):
+ return value
+ case .bool(let value):
+ return "\(value)"
+ case .float(let value):
+ return "\(value)"
+ }
+ }
+
+ }
+
+}
+
+extension Filter.Facet.ValueType: ExpressibleByBooleanLiteral {
+
+ public typealias BooleanLiteralType = Bool
+
+ public init(booleanLiteral value: BooleanLiteralType) {
+ self = .bool(value)
+ }
+
+}
+
+extension Filter.Facet.ValueType: ExpressibleByFloatLiteral {
+
+ public typealias FloatLiteralType = Float
+
+ public init(floatLiteral value: FloatLiteralType) {
+ self = .float(value)
+ }
+
+}
+
+extension Filter.Facet.ValueType: ExpressibleByStringLiteral {
+
+ public typealias StringLiterlalType = String
+
+ public init(stringLiteral value: StringLiteralType) {
+ self = .string(value)
+ }
+
+}
+
+extension Filter.Facet.ValueType: ExpressibleByIntegerLiteral {
+
+ public typealias IntegerLiteralType = Int
+
+ public init(integerLiteral value: IntegerLiteralType) {
+ self = .float(Float(value))
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Filter.swift b/Sources/InstantSearchCore/FilterState/FIlter/Filter.swift
new file mode 100644
index 00000000..38875427
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Filter.swift
@@ -0,0 +1,111 @@
+//
+// Filter.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public enum Filter: Hashable {
+
+ case facet(Facet)
+ case numeric(Numeric)
+ case tag(Tag)
+
+ public var attribute: Attribute {
+ switch self {
+ case .facet(let filter):
+ return filter.attribute
+ case .numeric(let filter):
+ return filter.attribute
+ case .tag(let filter):
+ return filter.attribute
+ }
+ }
+
+ public init(_ filter: F) {
+ switch filter {
+ case let facetFilter as Filter.Facet:
+ self = .facet(facetFilter)
+ case let numericFilter as Filter.Numeric:
+ self = .numeric(numericFilter)
+ case let tagFilter as Filter.Tag:
+ self = .tag(tagFilter)
+ default:
+ fatalError("Filter of type \(F.self) is not supported")
+ }
+ }
+
+ public init(_ filter: FilterType) {
+ switch filter {
+ case let facetFilter as Filter.Facet:
+ self = .facet(facetFilter)
+ case let numericFilter as Filter.Numeric:
+ self = .numeric(numericFilter)
+ case let tagFilter as Filter.Tag:
+ self = .tag(tagFilter)
+ default:
+ fatalError("Filter of type \(FilterType.self) is not supported")
+ }
+ }
+
+ public var filter: FilterType {
+ switch self {
+ case .facet(let facetFilter):
+ return facetFilter
+
+ case .numeric(let numericFilter):
+ return numericFilter
+
+ case .tag(let tagFilter):
+ return tagFilter
+ }
+ }
+
+}
+
+extension Filter: CustomStringConvertible {
+
+ public var description: String {
+ switch self {
+ case .facet(let facetFilter):
+ return facetFilter.description
+ case .numeric(let numericFilter):
+ return numericFilter.description
+ case .tag(let tagFilter):
+ return tagFilter.description
+ }
+ }
+
+}
+
+/// Abstract filter protocol
+public protocol FilterType {
+
+ /// Identifier of field affected by filter
+ var attribute: Attribute { get }
+
+ /// A Boolean value indicating whether filter is inverted
+ var isNegated: Bool { get set }
+
+ /// Replaces isNegated property by a new value
+ /// parameter value: new value of isNegated
+ mutating func not(value: Bool)
+
+}
+
+public extension FilterType {
+
+ mutating func not(value: Bool = true) {
+ isNegated = value
+ }
+
+}
+
+@discardableResult public prefix func ! (filter: T) -> T {
+ var mutableFilterCopy = filter
+ mutableFilterCopy.not(value: !filter.isNegated)
+ return mutableFilterCopy
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/AndFilterGroup.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/AndFilterGroup.swift
new file mode 100644
index 00000000..f994574a
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/AndFilterGroup.swift
@@ -0,0 +1,47 @@
+//
+// AndFilterGroup.swift
+// AlgoliaSearch OSX
+//
+// Created by Vladislav Fitc on 14/01/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/// Representation of conjunctive group of filters
+
+extension FilterGroup {
+
+ public struct And: FilterGroupType {
+
+ public var filters: [FilterType]
+ public let name: String?
+
+ public var isDisjuncitve: Bool {
+ return false
+ }
+
+ public init(filters: S, name: String? = nil) where S.Element == FilterType {
+ self.filters = Array(filters)
+ self.name = name
+ }
+
+ public static func and(_ filters: [FilterType]) -> FilterGroup.And {
+ return FilterGroup.And(filters: filters)
+ }
+
+ public func withFilters(_ filters: S) -> FilterGroup.And where S.Element == FilterType {
+ return .init(filters: filters, name: name)
+ }
+
+ }
+
+}
+
+extension FilterGroup.And: CustomStringConvertible {
+
+ public var description: String {
+ return "{ \(name ?? "_"): \(filters) }"
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/FilterGroupType.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/FilterGroupType.swift
new file mode 100644
index 00000000..ff9530f2
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/FilterGroupType.swift
@@ -0,0 +1,37 @@
+//
+// FilterGroup.swift
+// AlgoliaSearch
+//
+// Created by Guy Daher on 14/12/2018.
+// Copyright © 2018 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public enum FilterGroup {}
+
+public protocol FilterGroupType {
+
+ var name: String? { get }
+ var filters: [FilterType] { get }
+ var isDisjuncitve: Bool { get }
+
+ func withFilters(_ filters: S) -> Self where S.Element == FilterType
+
+}
+
+extension FilterGroupType {
+
+ var isConjunctive: Bool {
+ return !isDisjuncitve
+ }
+
+ public var isEmpty: Bool {
+ return filters.isEmpty
+ }
+
+ func contains(_ filter: FilterType) -> Bool {
+ return filters.contains(where: { Filter($0) == Filter(filter) })
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/HierarchicalFilterGroup.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/HierarchicalFilterGroup.swift
new file mode 100644
index 00000000..4035e96b
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/HierarchicalFilterGroup.swift
@@ -0,0 +1,48 @@
+//
+// HierarchicalFilterGroup.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FilterGroup {
+
+ public struct Hierarchical: FilterGroupType {
+
+ public var filters: [FilterType] {
+ return typedFilters
+ }
+
+ public let name: String?
+
+ public var isDisjuncitve: Bool {
+ return false
+ }
+
+ public var hierarchicalAttributes: [Attribute] = []
+ public var hierarchicalFilters: [Filter.Facet] = []
+
+ internal var typedFilters: [Filter.Facet]
+
+ public init(filters: S, name: String? = nil) where S.Element == Filter.Facet {
+ self.typedFilters = Array(filters)
+ self.name = name
+ }
+
+ public static func hierarchical(_ filters: [Filter.Facet]) -> FilterGroup.Hierarchical {
+ return FilterGroup.Hierarchical(filters: filters)
+ }
+
+ public func withFilters(_ filters: S) -> FilterGroup.Hierarchical where S.Element == FilterType {
+ var updatedGroup = FilterGroup.Hierarchical(filters: filters.compactMap { $0 as? Filter.Facet }, name: name)
+ updatedGroup.hierarchicalAttributes = hierarchicalAttributes
+ updatedGroup.hierarchicalFilters = hierarchicalFilters
+ return updatedGroup
+ }
+
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/OrFilterGroup.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/OrFilterGroup.swift
new file mode 100644
index 00000000..159e779f
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/OrFilterGroup.swift
@@ -0,0 +1,53 @@
+//
+// OrFilterGroup.swift
+// AlgoliaSearch OSX
+//
+// Created by Vladislav Fitc on 14/01/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+// swiftlint:disable type_name
+
+import Foundation
+
+/// Representation of disjunctive group of filters
+
+extension FilterGroup {
+
+ public struct Or: FilterGroupType {
+
+ public var filters: [FilterType] {
+ return typedFilters
+ }
+
+ public let name: String?
+
+ public var isDisjuncitve: Bool {
+ return true
+ }
+
+ internal var typedFilters: [T]
+
+ public init(filters: [T] = [], name: String? = nil) {
+ self.typedFilters = filters
+ self.name = name
+ }
+
+ public static func or(_ filters: [T]) -> FilterGroup.Or {
+ return FilterGroup.Or(filters: filters)
+ }
+
+ public func withFilters(_ filters: S) -> Or where S.Element == FilterType {
+ return .init(filters: filters.compactMap { $0 as? T }, name: name)
+ }
+
+ }
+
+}
+
+extension FilterGroup.Or: CustomStringConvertible {
+
+ public var description: String {
+ return "{ \(name ?? "_"): \(filters) }"
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/NumericFilter.swift b/Sources/InstantSearchCore/FilterState/FIlter/NumericFilter.swift
new file mode 100644
index 00000000..810918db
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/NumericFilter.swift
@@ -0,0 +1,91 @@
+//
+// Numeric.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/** Defines filter representing a numeric relation or a range
+ # See also:
+ [Reference](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-numeric-value/)
+ */
+
+public extension Filter {
+
+ struct Numeric: FilterType, Equatable {
+
+ public enum ValueType: Hashable {
+ case range(ClosedRange)
+ case comparison(Operator, Double)
+ }
+
+ public enum Operator: String, CustomStringConvertible {
+ case lessThan = "<"
+ case lessThanOrEqual = "<="
+ case equals = "="
+ case notEquals = "!="
+ case greaterThanOrEqual = ">="
+ case greaterThan = ">"
+
+ var inversion: Operator {
+ switch self {
+ case .equals:
+ return .notEquals
+ case .greaterThan:
+ return .lessThanOrEqual
+ case .greaterThanOrEqual:
+ return .lessThan
+ case .lessThan:
+ return .greaterThanOrEqual
+ case .lessThanOrEqual:
+ return .greaterThan
+ case .notEquals:
+ return .equals
+ }
+ }
+
+ public var description: String {
+ return rawValue
+ }
+
+ }
+
+ public let attribute: Attribute
+ public let value: ValueType
+ public var isNegated: Bool
+
+ init(attribute: Attribute, value: ValueType, isNegated: Bool) {
+ self.attribute = attribute
+ self.isNegated = isNegated
+ self.value = value
+ }
+
+ public init(attribute: Attribute, range: ClosedRange, isNegated: Bool = false) {
+ self.init(attribute: attribute, value: .range(range), isNegated: isNegated)
+ }
+
+ public init(attribute: Attribute, `operator`: Operator, value: Double, isNegated: Bool = false) {
+ self.init(attribute: attribute, value: .comparison(`operator`, value), isNegated: isNegated)
+ }
+
+ }
+
+}
+
+extension Filter.Numeric: CustomStringConvertible {
+
+ public var description: String {
+ switch value {
+ case .range(let range):
+ return "\(attribute): \(range.lowerBound) – \(range.upperBound)"
+ case .comparison(let compOperator, let value):
+ return "\(attribute) \(compOperator.description) \(value)"
+ }
+ }
+
+}
+
+extension Filter.Numeric: Hashable {}
diff --git a/Sources/InstantSearchCore/FilterState/FIlter/TagFilter.swift b/Sources/InstantSearchCore/FilterState/FIlter/TagFilter.swift
new file mode 100644
index 00000000..1419a2b2
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FIlter/TagFilter.swift
@@ -0,0 +1,51 @@
+//
+// Tag.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/** Defines tag filter
+ # See also:
+ [Reference](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-tags/)
+ */
+
+public extension Filter {
+
+ struct Tag: FilterType, Equatable {
+
+ public let attribute: Attribute = .tags
+ public var isNegated: Bool
+ public let value: String
+
+ public init(value: String, isNegated: Bool = false) {
+ self.isNegated = isNegated
+ self.value = value
+ }
+
+ }
+
+}
+
+extension Filter.Tag: Hashable {}
+
+extension Filter.Tag: ExpressibleByStringLiteral {
+
+ public typealias StringLiteralType = String
+
+ public init(stringLiteral string: String) {
+ self.init(value: string, isNegated: false)
+ }
+
+}
+
+extension Filter.Tag: CustomStringConvertible {
+
+ public var description: String {
+ return "\(value)"
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterGroupID.swift b/Sources/InstantSearchCore/FilterState/FilterGroupID.swift
new file mode 100644
index 00000000..1211dc50
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterGroupID.swift
@@ -0,0 +1,96 @@
+//
+// FilterGroupID.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 10/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+// swiftlint:disable type_name
+
+import Foundation
+
+extension FilterGroup {
+
+ public enum ID: Hashable {
+
+ public enum Filter {
+ case facet, numeric, tag
+
+ init?(_ filterType: F.Type) {
+ switch filterType {
+ case is FacetFilter.Type:
+ self = .facet
+ case is NumericFilter.Type:
+ self = .numeric
+ case is TagFilter.Type:
+ self = .tag
+ default:
+ return nil
+ }
+ }
+
+ }
+
+ case or(name: String, filterType: Filter)
+ case and(name: String)
+ case hierarchical(name: String)
+
+ var name: String {
+ switch self {
+ case .or(name: let name, _),
+ .and(name: let name),
+ .hierarchical(name: let name):
+ return name
+ }
+ }
+
+ var isConjunctive: Bool {
+ switch self {
+ case .and,
+ .hierarchical:
+ return true
+ case .or:
+ return false
+ }
+ }
+
+ var isDisjunctive: Bool {
+ return !isConjunctive
+ }
+
+ init?(_ filterGroup: FilterGroupType) {
+ let groupName = filterGroup.name ?? ""
+ switch filterGroup {
+ case is FilterGroup.And:
+ self = .and(name: groupName)
+ case is FilterGroup.Hierarchical:
+ self = .hierarchical(name: groupName)
+ case is FilterGroup.Or:
+ self = .or(name: groupName, filterType: .facet)
+ case is FilterGroup.Or:
+ self = .or(name: groupName, filterType: .numeric)
+ case is FilterGroup.Or:
+ self = .or(name: groupName, filterType: .tag)
+ default:
+ return nil
+ }
+ }
+
+ }
+
+}
+
+extension FilterGroup.ID: CustomStringConvertible {
+
+ public var description: String {
+ switch self {
+ case .and(name: let name):
+ return "and<\(name)>"
+ case .or(name: let name, _):
+ return "or<\(name)>"
+ case .hierarchical(name: let name):
+ return "hierarchical<\(name)>"
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterGroupsConvertible.swift b/Sources/InstantSearchCore/FilterState/FilterGroupsConvertible.swift
new file mode 100644
index 00000000..3a160797
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterGroupsConvertible.swift
@@ -0,0 +1,15 @@
+//
+// FilterGroupsConvertible.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 18/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol FilterGroupsConvertible {
+
+ func toFilterGroups() -> [FilterGroupType]
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterState+Commands.swift b/Sources/InstantSearchCore/FilterState/FilterState+Commands.swift
new file mode 100644
index 00000000..e577bb07
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterState+Commands.swift
@@ -0,0 +1,157 @@
+//
+// FilterState+Commands.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 07/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+private protocol FilterStateCommand {
+
+ func execute(on filterState: FilterState)
+
+}
+
+private extension FilterState {
+
+ struct Add: FilterStateCommand {
+
+ public let groupID: FilterGroup.ID
+ public let filters: [T]
+
+ public init(filter: T, groupID: FilterGroup.ID) {
+ self.init(filters: [filter], groupID: groupID)
+ }
+
+ public init(filters: S, groupID: FilterGroup.ID) where S.Element == T {
+ self.groupID = groupID
+ self.filters = Array(filters)
+ }
+
+ public func execute(on filterState: FilterState) {
+ filterState.addAll(filters: filters, toGroupWithID: groupID)
+ }
+
+ }
+
+ struct Remove: FilterStateCommand {
+
+ public let filters: [T]
+ public let groupID: FilterGroup.ID?
+
+ public init(filter: T, groupID: FilterGroup.ID? = nil) {
+ self.init(filters: [filter], groupID: groupID)
+ }
+
+ public init(filters: [T], groupID: FilterGroup.ID? = nil) {
+ self.groupID = groupID
+ self.filters = filters
+ }
+
+ public func execute(on filterState: FilterState) {
+
+ if let groupID = groupID {
+ filterState.removeAll(filters, fromGroupWithID: groupID)
+ } else {
+ _ = filterState.removeAll(filters)
+ }
+ }
+
+ }
+
+ enum Clear: FilterStateCommand {
+
+ case all
+ case group(FilterGroup.ID)
+ case attribute(Attribute)
+ case attributeInGroup(Attribute, FilterGroup.ID)
+
+ public func execute(on filterState: FilterState) {
+
+ switch self {
+ case .all:
+ filterState.removeAll()
+ case .attribute(let attribute):
+ filterState.removeAll(for: attribute)
+ case .attributeInGroup(let attribute, let groupID):
+ filterState.removeAll(for: attribute, fromGroupWithID: groupID)
+ case .group(let groupID):
+ filterState.removeAll(fromGroupWithID: groupID)
+ }
+
+ }
+
+ }
+
+ struct Toggle: FilterStateCommand {
+
+ public let groupID: FilterGroup.ID
+ public let filters: [T]
+
+ public init(filter: T, groupID: FilterGroup.ID) {
+ self.init(filters: [filter], groupID: groupID)
+ }
+
+ public init(filters: [T], groupID: FilterGroup.ID) {
+ self.groupID = groupID
+ self.filters = filters
+ }
+
+ public func execute(on filterState: FilterState) {
+ filterState.toggle(filters, inGroupWithID: groupID)
+ }
+
+ }
+
+}
+
+public extension FilterState {
+
+ struct Command {
+
+ fileprivate let command: FilterStateCommand
+
+ public static func add(filter: T, toGroupWithID groupID: FilterGroup.ID) -> Command {
+ return .init(command: FilterState.Add(filter: filter, groupID: groupID))
+ }
+
+ public static func add(filters: S, toGroupWithID groupID: FilterGroup.ID) -> Command where S.Element == T {
+ return .init(command: FilterState.Add(filters: filters, groupID: groupID))
+ }
+
+ public static func remove(filter: T, fromGroupWithID groupID: FilterGroup.ID) -> Command {
+ return .init(command: FilterState.Remove(filter: filter, groupID: groupID))
+ }
+
+ public static func remove(filters: [T], fromGroupWithID groupID: FilterGroup.ID) -> Command {
+ return .init(command: FilterState.Remove(filters: filters, groupID: groupID))
+ }
+
+ public static var removeAll: Command = .init(command: FilterState.Clear.all)
+
+ public static func removeAll(fromGroupWithID groupID: FilterGroup.ID) -> Command {
+ return .init(command: FilterState.Clear.group(groupID))
+ }
+
+ public static func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID? = nil) -> Command {
+ return .init(command: FilterState.Clear.attribute(attribute))
+ }
+
+ public static func toggle(filter: T, toGroupWithID groupID: FilterGroup.ID) -> Command {
+ return .init(command: FilterState.Toggle(filter: filter, groupID: groupID))
+ }
+
+ public static func toggle(filters: [T], toGroupWithID groupID: FilterGroup.ID) -> Command {
+ return .init(command: FilterState.Toggle(filters: filters, groupID: groupID))
+ }
+
+ }
+
+ func notify(_ commands: Command...) {
+ commands.forEach { $0.command.execute(on: self) }
+ onChange.fire(ReadOnlyFiltersContainer(filterState: self))
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterState+DisjunctiveFaceting.swift b/Sources/InstantSearchCore/FilterState/FilterState+DisjunctiveFaceting.swift
new file mode 100644
index 00000000..e40e88dc
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterState+DisjunctiveFaceting.swift
@@ -0,0 +1,17 @@
+//
+// FilterState+DisjunctiveFaceting.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 05/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FilterState: DisjunctiveFacetingDelegate {
+
+ public var disjunctiveFacetsAttributes: Set {
+ return filters.disjunctiveFacetsAttributes
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterState+FiltersReadable.swift b/Sources/InstantSearchCore/FilterState/FilterState+FiltersReadable.swift
new file mode 100644
index 00000000..ccdd50b3
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterState+FiltersReadable.swift
@@ -0,0 +1,41 @@
+//
+// FilterState+FiltersReadable.swift
+// InstantSearchCore-iOS
+//
+// Created by Vladislav Fitc on 19/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FilterState: FiltersReadable {
+
+ func getGroupIDs() -> Set {
+ return filters.getGroupIDs()
+ }
+
+ public var isEmpty: Bool {
+ return filters.isEmpty
+ }
+
+ func contains(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) -> Bool {
+ return filters.contains(filter, inGroupWithID: groupID)
+ }
+
+ func getFilters(forGroupWithID groupID: FilterGroup.ID) -> Set {
+ return filters.getFilters(forGroupWithID: groupID)
+ }
+
+ public func getFilters(for attribute: Attribute) -> Set {
+ return filters.getFilters(for: attribute)
+ }
+
+ public func getFilters() -> Set {
+ return filters.getFilters()
+ }
+
+ public func getFiltersAndID() -> Set {
+ return filters.getFiltersAndID()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterState+FiltersWritable.swift b/Sources/InstantSearchCore/FilterState/FilterState+FiltersWritable.swift
new file mode 100644
index 00000000..4c7704d5
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterState+FiltersWritable.swift
@@ -0,0 +1,69 @@
+//
+// FilterState+FiltersWritable.swift
+// InstantSearchCore-iOS
+//
+// Created by Vladislav Fitc on 19/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FilterState: FiltersWritable {
+
+ func add(_ filter: FilterType, toGroupWithID groupID: FilterGroup.ID) {
+ filters.add(filter, toGroupWithID: groupID)
+ }
+
+ func addAll(filters: S, toGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType {
+ self.filters.addAll(filters: filters, toGroupWithID: groupID)
+ }
+
+ @discardableResult func remove(_ filter: FilterType, fromGroupWithID groupID: FilterGroup.ID) -> Bool {
+ return filters.remove(filter, fromGroupWithID: groupID)
+ }
+
+ @discardableResult func removeAll(_ filters: S, fromGroupWithID groupID: FilterGroup.ID) -> Bool where S.Element == FilterType {
+ return self.filters.removeAll(filters, fromGroupWithID: groupID)
+ }
+
+ func removeAll(fromGroupWithID groupID: FilterGroup.ID) {
+ return filters.removeAll(fromGroupWithID: groupID)
+ }
+
+ func removeAll(fromGroupWithIDs groupIDs: [FilterGroup.ID]) {
+ return filters.removeAll(fromGroupWithIDs: groupIDs)
+ }
+
+ func removeAllExcept(fromGroupWithIDs groupIDs: [FilterGroup.ID]) {
+ return filters.removeAllExcept(groupIDs)
+ }
+
+ @discardableResult public func remove(_ filter: FilterType) -> Bool {
+ return filters.remove(filter)
+ }
+
+ @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == FilterType {
+ return self.filters.removeAll(filters)
+ }
+
+ func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID) {
+ filters.removeAll(for: attribute, fromGroupWithID: groupID)
+ }
+
+ public func removeAll(for attribute: Attribute) {
+ filters.removeAll(for: attribute)
+ }
+
+ public func removeAll() {
+ filters.removeAll()
+ }
+
+ func toggle(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) {
+ filters.toggle(filter, inGroupWithID: groupID)
+ }
+
+ func toggle(_ filters: S, inGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType {
+ self.filters.toggle(filters, inGroupWithID: groupID)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterState+HierarchicalFaceting.swift b/Sources/InstantSearchCore/FilterState/FilterState+HierarchicalFaceting.swift
new file mode 100644
index 00000000..d85cfbf5
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterState+HierarchicalFaceting.swift
@@ -0,0 +1,38 @@
+//
+// FilterState+HierarchicalFaceting.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 05/08/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+extension FilterState: HierarchicalFacetingDelegate {
+
+ private var hierarchicalGroupName: String {
+ return "_hierarchical"
+ }
+
+ public var hierarchicalFilters: [Filter.Facet] {
+ get {
+ return self[hierarchical: hierarchicalGroupName].hierarchicalFilters
+ }
+
+ set {
+ self[hierarchical: hierarchicalGroupName].set(newValue)
+
+ }
+ }
+
+ public var hierarchicalAttributes: [Attribute] {
+ get {
+ return self[hierarchical: hierarchicalGroupName].hierarchicalAttributes
+ }
+
+ set {
+ self[hierarchical: hierarchicalGroupName].set(newValue)
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterState.swift b/Sources/InstantSearchCore/FilterState/FilterState.swift
new file mode 100644
index 00000000..a4c7f1f8
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterState.swift
@@ -0,0 +1,112 @@
+//
+// FilterState.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 18/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+/**
+ Encapsulates search filters providing a convenient interface to manage them
+ */
+public class FilterState {
+
+ typealias Storage = FiltersReadable & FiltersWritable & FilterGroupsConvertible & HierarchicalManageable
+
+ /// Filters container
+ var filters: Storage
+
+ /// Triggered when an error occured during search query execution
+ /// - Parameter: a tuple of query and error
+ public var onChange: Observer
+
+ /// Default constructor
+ public init() {
+ self.filters = GroupsStorage()
+ self.onChange = .init()
+ }
+
+ /// Copy constructor
+ public init(_ filterState: FilterState) {
+ self.filters = filterState.filters
+ self.onChange = .init()
+ }
+
+ /// Replace the groups of filter state by the groups of the filter state passed as parameter
+ /// - Parameter filterState: source filter state
+ public func setWithContent(of filterState: FilterState) {
+ self.filters = filterState.filters
+ }
+
+ /// Force trigger onChange event
+ public func notifyChange() {
+ onChange.fire(ReadOnlyFiltersContainer(filterState: self))
+ }
+
+ /// Subscript providing access to a conjunctive group with specified name
+ /// - Parameter groupName: required group name
+ /// - Returns: required group accessor
+ public subscript(and groupName: String) -> AndGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+ /// Subscript providing access to disjunctive group with specified name and manually defined filter type
+ /// To use if filter type cannot be inferred
+ /// - Parameter groupName: required group name
+ /// - Returns: required group accessor
+ public subscript(or groupName: String, type: F.Type) -> OrGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+ /// Subscript providing access to a disjunctive group with specified name
+ /// - Parameter groupName: required group name
+ /// - Returns: required group accessor
+ public subscript(or groupName: String) -> OrGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+ /// Subscript providing access to a hierarchical group with specified name
+ /// - Parameter groupName: required group name
+ /// - Returns: required group accessor
+ public subscript(hierarchical groupName: String) -> HierarchicalGroupAccessor {
+ return .init(filtersContainer: self, groupName: groupName)
+ }
+
+}
+
+extension FilterState: FiltersContainer {}
+
+extension FilterState: FilterGroupsConvertible {
+
+ public func toFilterGroups() -> [FilterGroupType] {
+ return filters.toFilterGroups()
+ }
+
+}
+
+extension FilterState: CustomStringConvertible {
+
+ public var description: String {
+ return FilterGroupConverter().sql(toFilterGroups()) ?? ""
+ }
+
+}
+
+extension FilterState: CustomDebugStringConvertible {
+
+ public var debugDescription: String {
+ let filterGroups = toFilterGroups()
+ guard !filterGroups.isEmpty else {
+ return "FilterState {}"
+ }
+ let body = filterGroups.map { group in
+ let groupName = (group.name ?? "")
+ let filtersDescription = FilterGroupConverter().sql(group) ?? ""
+ return " \"\(groupName)\": \(filtersDescription)"
+ }.joined(separator: "\n")
+ return "FilterState {\n\(body)\n}"
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FilterStateDSL.swift b/Sources/InstantSearchCore/FilterState/FilterStateDSL.swift
new file mode 100644
index 00000000..e250eab4
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FilterStateDSL.swift
@@ -0,0 +1,19 @@
+//
+// FilterStateDSL.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 15/04/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class FilterStateDSL {
+
+ var filters: FiltersReadable & FiltersWritable & FilterGroupsConvertible
+
+ public init() {
+ self.filters = GroupsStorage()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FiltersReadable.swift b/Sources/InstantSearchCore/FilterState/FiltersReadable.swift
new file mode 100644
index 00000000..159f9110
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FiltersReadable.swift
@@ -0,0 +1,81 @@
+//
+// FiltersReadable.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 16/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+protocol FiltersReadable {
+
+ /// A Boolean value indicating whether FilterState contains at least on filter
+
+ var isEmpty: Bool { get }
+
+ /// Tests whether FilterState contains a filter
+ /// - parameter filter: desired filter
+
+ func contains(_ filter: FilterType) -> Bool
+
+ /// Checks whether specified group contains a filter
+ /// - parameter filter: filter to check
+ /// - parameter groupID: target group ID
+ /// - returns: true if filter is contained by specified group
+
+ func contains(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) -> Bool
+
+ /// Returns a set of filters in group with specified ID
+ /// - parameter groupID: target group ID
+
+ func getGroupIDs() -> Set
+
+ /// Returns a set of filters for attribute
+ /// - parameter attribute: target attribute
+
+ func getFilters(forGroupWithID groupID: FilterGroup.ID) -> Set
+
+ func getFilters(for attribute: Attribute) -> Set
+
+ /// Returns a set of all the filters contained by all the groups
+ func getFilters() -> Set
+
+}
+
+extension FiltersReadable {
+
+ func contains(_ filter: FilterType) -> Bool {
+ return getGroupIDs().anySatisfy { contains(filter, inGroupWithID: $0) }
+ }
+
+ func getFiltersAndID() -> Set {
+ return Set(getGroupIDs()
+ .map { groupID in
+ getFilters(forGroupWithID: groupID).map { (groupID, $0) }
+ }
+ .flatMap { $0 }
+ .map { FilterAndID(filter: $0.1, id: $0.0) })
+ }
+
+}
+
+extension FiltersReadable {
+
+ var disjunctiveFacetsAttributes: Set {
+ let attributes = getGroupIDs()
+ .filter { groupID in
+ switch groupID {
+ case .or(_, .facet):
+ return true
+ default:
+ return false
+ }
+ }
+ .map(getFilters(forGroupWithID:))
+ .flatMap({ $0 })
+ .map { $0.attribute }
+ return Set(attributes)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/FiltersWritable.swift b/Sources/InstantSearchCore/FilterState/FiltersWritable.swift
new file mode 100644
index 00000000..93797fb1
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/FiltersWritable.swift
@@ -0,0 +1,127 @@
+//
+// FiltersWritable.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 16/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+protocol FiltersWritable {
+
+ /// Adds filter to a specified group
+ /// - parameter filter: filter to add
+ /// - parameter groupID: target group ID
+
+ mutating func add(_ filter: FilterType, toGroupWithID groupID: FilterGroup.ID)
+
+ /// Adds a sequence of filters to a specified group
+ /// - parameter filters: sequence of filters to add
+ /// - parameter groupID: target group ID
+
+ mutating func addAll(filters: S, toGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType
+
+ /// Removes filter from a specified group
+ /// - parameter filter: filter to remove
+ /// - parameter groupID: target group ID
+ /// - returns: true if removal succeeded, otherwise returns false
+
+ @discardableResult mutating func remove(_ filter: FilterType, fromGroupWithID groupID: FilterGroup.ID) -> Bool
+
+ /// Removes a sequence of filters from a specified group
+ /// - parameter filters: sequence of filters to remove
+ /// - parameter groupID: target group ID
+ /// - returns: true if at least one filter in filters sequence is contained by a specified group and so has been removed, otherwise returns false
+
+ @discardableResult mutating func removeAll(_ filters: S, fromGroupWithID groupID: FilterGroup.ID) -> Bool where S.Element == FilterType
+
+ /// Removes all filters from a specifed group
+ /// - parameter group: target group ID
+
+ mutating func removeAll(fromGroupWithID groupID: FilterGroup.ID)
+
+ mutating func removeAll(fromGroupWithIDs groupIDs: [FilterGroup.ID])
+
+ /// Removes filter from all the groups
+ /// - parameter filter: filter to remove
+ /// - returns: true if specified filter has been removed from at least one group, otherwise returns false
+
+ @discardableResult mutating func remove(_ filter: FilterType) -> Bool
+
+ /// Removes a sequence of filters from all the groups
+ /// - parameter filters: sequence of filters to remove
+
+ mutating func removeAll(_ filters: S) -> Bool where S.Element == FilterType
+
+ /// Removes all filters with specified attribute in a specified group
+ /// - parameter attribute: target attribute
+ /// - parameter groupID: target group ID
+
+ mutating func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID)
+
+ /// Removes all filters with specified attribute in all the groups
+ /// - parameter attribute: target attribute
+
+ mutating func removeAll(for attribute: Attribute)
+
+ /// Removes all filters from all the groups
+
+ mutating func removeAll()
+
+ /// Removes filter from group if contained by it, otherwise adds filter to group
+ /// - parameter filter: filter to toggle
+ /// - parameter groupID: target group ID
+
+ mutating func toggle(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID)
+
+ /// Toggles a sequence of filters in group
+ /// - parameter filters: sequence of filters to toggle
+ /// - parameter groupID: target group ID
+
+ mutating func toggle(_ filters: S, inGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType
+
+}
+
+extension FiltersWritable {
+
+ mutating func add(_ filter: FilterType, toGroupWithID groupID: FilterGroup.ID) {
+ addAll(filters: [filter], toGroupWithID: groupID)
+ }
+
+ @discardableResult mutating func remove(_ filter: FilterType, fromGroupWithID groupID: FilterGroup.ID) -> Bool {
+ return removeAll([filter], fromGroupWithID: groupID)
+ }
+
+ @discardableResult mutating func remove(_ filter: FilterType) -> Bool {
+ return removeAll([filter])
+ }
+
+ mutating func removeAll(fromGroupWithID groupID: FilterGroup.ID) {
+ removeAll(fromGroupWithIDs: [groupID])
+ }
+
+ mutating func toggle(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) {
+ toggle([filter], inGroupWithID: groupID)
+ }
+
+}
+
+extension FiltersWritable where Self: FiltersReadable {
+
+ mutating func toggle(_ filters: S, inGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType {
+ for filter in filters {
+ if contains(filter, inGroupWithID: groupID) {
+ _ = remove(filter, fromGroupWithID: groupID)
+ } else {
+ add(filter, toGroupWithID: groupID)
+ }
+ }
+ }
+
+ mutating func removeAllExcept(_ groupIDsToKeep: [FilterGroup.ID]) {
+ let groupIDsToRemove = getGroupIDs().filter { !groupIDsToKeep.contains($0) }
+ removeAll(fromGroupWithIDs: Array(groupIDsToRemove))
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/GroupStorage.swift b/Sources/InstantSearchCore/FilterState/GroupStorage.swift
new file mode 100644
index 00000000..33ab5996
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/GroupStorage.swift
@@ -0,0 +1,260 @@
+//
+// GroupStorage.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 15/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+struct GroupsStorage {
+
+ var filterGroups: [FilterGroup.ID: FilterGroupType]
+
+ init() {
+ filterGroups = [:]
+ }
+
+}
+
+extension GroupsStorage: FilterGroupsConvertible {
+
+ func toFilterGroups() -> [FilterGroupType] {
+
+ let filterComparator: (FilterType, FilterType) -> Bool = {
+ let converter = FilterConverter()
+ let lhsString = converter.sql($0)!
+ let rhsString = converter.sql($1)!
+ return lhsString < rhsString
+ }
+
+ let groupIDComparator: (FilterGroup.ID, FilterGroup.ID) -> Bool = {
+ guard $0.name != $1.name else {
+ switch ($0, $1) {
+ case (.or, .and):
+ return true
+ default:
+ return false
+ }
+ }
+ return $0.name < $1.name
+ }
+
+ let transform: (FilterGroup.ID, FilterGroupType) -> FilterGroupType = { (groupID, filterGroup) in
+
+ let sortedFilters = filterGroup.filters.sorted(by: filterComparator)
+
+ switch groupID {
+ case .and:
+ return FilterGroup.And(filters: sortedFilters, name: groupID.name)
+ case .hierarchical:
+ return FilterGroup.And(filters: sortedFilters.compactMap { $0 as? Filter.Facet }, name: groupID.name)
+ case .or(_, .facet):
+ return FilterGroup.Or(filters: sortedFilters.compactMap { $0 as? Filter.Facet }, name: groupID.name)
+ case .or(_, .tag):
+ return FilterGroup.Or(filters: sortedFilters.compactMap { $0 as? Filter.Tag }, name: groupID.name)
+ case .or(_, .numeric):
+ return FilterGroup.Or(filters: sortedFilters.compactMap { $0 as? Filter.Numeric }, name: groupID.name)
+ }
+
+ }
+
+ return filterGroups
+ .sorted(by: { groupIDComparator($0.key, $1.key) })
+ .compactMap(transform)
+ .filter { !$0.filters.isEmpty }
+
+ }
+
+}
+
+extension GroupsStorage: FiltersReadable {
+
+ func getFilters(forGroupWithID groupID: FilterGroup.ID) -> Set {
+ return Set(filterGroups[groupID]?.filters.map(Filter.init) ?? [])
+ }
+
+ func getFilters(for attribute: Attribute) -> Set {
+ return Set(getFilters().filter { $0.attribute == attribute })
+ }
+
+ func getFiltersAndID() -> Set {
+ return Set(filterGroups
+ .map { (id, group) in group.filters.map { (id, $0) } }
+ .flatMap { $0 }
+ .map { FilterAndID(filter: Filter($0.1), id: $0.0) })
+ }
+
+ func getFilters() -> Set {
+ return Set(filterGroups.values.flatMap { $0.filters }.map(Filter.init))
+ }
+
+ var isEmpty: Bool {
+ return filterGroups.values.allSatisfy { $0.filters.isEmpty }
+ }
+
+ func contains(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) -> Bool {
+ return filterGroups[groupID]?.contains(filter) == true
+ }
+
+ func getGroupIDs() -> Set {
+ return Set(filterGroups.keys)
+ }
+
+}
+
+extension GroupsStorage: FiltersWritable {
+
+ private func emptyGroup(with filterGroupID: FilterGroup.ID) -> FilterGroupType {
+ switch filterGroupID {
+ case .and(name: let name):
+ return FilterGroup.And(filters: [], name: name)
+ case .hierarchical(name: let name):
+ return FilterGroup.Hierarchical(filters: [], name: name)
+ case .or(name: let name, filterType: .facet):
+ return FilterGroup.Or(filters: [], name: name)
+ case .or(name: let name, filterType: .numeric):
+ return FilterGroup.Or(filters: [], name: name)
+ case .or(name: let name, filterType: .tag):
+ return FilterGroup.Or(filters: [], name: name)
+ }
+ }
+
+ mutating func addAll(filters: S, toGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType {
+ let group = filterGroups[groupID] ?? emptyGroup(with: groupID)
+ let updatedFilters = Set(group.filters.map(Filter.init)).union(filters.map(Filter.init)).map { $0.filter }
+ filterGroups[groupID] = group.withFilters(updatedFilters)
+ }
+
+ @discardableResult mutating func removeAll(_ filters: S, fromGroupWithID groupID: FilterGroup.ID) -> Bool where S.Element == FilterType {
+ guard let existingGroup = filterGroups[groupID] else {
+ return false
+ }
+
+ let filtersToRemove = filters.map(Filter.init)
+ let updatedFilters = existingGroup.filters.filter { !filtersToRemove.contains(Filter($0)) }
+ let updatedGroup = existingGroup.withFilters(updatedFilters)
+ if updatedGroup.isEmpty {
+ filterGroups.removeValue(forKey: groupID)
+ } else {
+ filterGroups[groupID] = updatedGroup
+ }
+ return existingGroup.filters.count > updatedFilters.count
+ }
+
+ mutating func removeAll(fromGroupWithIDs groupIDs: [FilterGroup.ID]) {
+ for groupID in groupIDs {
+ switch groupID {
+ case .hierarchical:
+ filterGroups[groupID] = filterGroups[groupID]?.withFilters([])
+ default:
+ filterGroups.removeValue(forKey: groupID)
+ }
+ }
+ }
+
+ @discardableResult mutating func removeAll(_ filters: S) -> Bool where S.Element == FilterType {
+ var wasRemoved = false
+ let filtersToRemove = Set(filters.map(Filter.init))
+ for (id, group) in filterGroups {
+ let updatedFilters = group.filters.filter { !filtersToRemove.contains(Filter($0)) }
+ filterGroups[id] = group.withFilters(updatedFilters)
+ wasRemoved = wasRemoved || updatedFilters.count < group.filters.count
+ }
+ return wasRemoved
+ }
+
+ mutating func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID) {
+ guard let existingGroup = filterGroups[groupID] else {
+ return
+ }
+
+ let updatedFilters = existingGroup.filters.filter { $0.attribute != attribute }
+ filterGroups[groupID] = existingGroup.withFilters(updatedFilters)
+ }
+
+ mutating func removeAll(for attribute: Attribute) {
+ for (groupID, group) in filterGroups {
+ let updatedFilters = group.filters.filter { $0.attribute != attribute }
+ filterGroups[groupID] = group.withFilters(updatedFilters)
+ }
+ }
+
+ mutating func removeAll() {
+ filterGroups.removeAll()
+ }
+
+}
+
+extension GroupsStorage: HierarchicalManageable {
+
+ func hierarchicalGroup(withName groupName: String) -> FilterGroup.Hierarchical? {
+ return filterGroups[.hierarchical(name: groupName)].flatMap { $0 as? FilterGroup.Hierarchical }
+ }
+
+ func hierarchicalAttributes(forGroupWithName groupName: String) -> [Attribute] {
+ return hierarchicalGroup(withName: groupName)?.hierarchicalAttributes ?? []
+ }
+
+ func hierarchicalFilters(forGroupWithName groupName: String) -> [Filter.Facet] {
+ return hierarchicalGroup(withName: groupName)?.hierarchicalFilters ?? []
+
+ }
+
+ mutating func set(_ hierarchicalAttributes: [Attribute], forGroupWithName groupName: String) {
+ let groupID: FilterGroup.ID = .hierarchical(name: groupName)
+ var updatedGroup: FilterGroup.Hierarchical = (filterGroups[groupID] as? FilterGroup.Hierarchical) ?? .init(filters: [], name: groupName)
+ updatedGroup.hierarchicalAttributes = hierarchicalAttributes
+ filterGroups[groupID] = updatedGroup
+ }
+
+ mutating func set(_ hierarchicalFilters: [Filter.Facet], forGroupWithName groupName: String) {
+ let groupID: FilterGroup.ID = .hierarchical(name: groupName)
+ var updatedGroup: FilterGroup.Hierarchical = (filterGroups[groupID] as? FilterGroup.Hierarchical) ?? .init(filters: [], name: groupName)
+ updatedGroup.hierarchicalFilters = hierarchicalFilters
+ filterGroups[groupID] = updatedGroup
+ }
+
+}
+
+extension GroupsStorage {
+
+ /// Returns a set of attributes suitable for disjunctive faceting
+ func getDisjunctiveFacetsAttributes() -> Set {
+ let attributes = filterGroups
+ .values
+ .filter { $0.isDisjuncitve }
+ .compactMap { $0.filters.compactMap { $0 as? Filter.Facet } }
+ .flatMap { $0 }
+ .map { $0.attribute }
+ return Set(attributes)
+
+ }
+
+ /// Returns a dictionary of all facet filters with their associated values
+ func getFacetFilters() -> [Attribute: Set] {
+ let facetFilters: [Filter.Facet] = filterGroups.values.flatMap { $0.filters.compactMap { $0 as? Filter.Facet } }
+
+ var refinements: [Attribute: Set] = [:]
+ for filter in facetFilters {
+ let existingValues = refinements[filter.attribute, default: []]
+ let updatedValues = existingValues.union([filter.value])
+ refinements[filter.attribute] = updatedValues
+ }
+ return refinements
+ }
+
+ /// Returns a raw representaton of all facet filters with their associated values
+ func getRawFacetFilters() -> [String: [String]] {
+ return getFacetFilters()
+ .map { ($0.key.rawValue, $0.value.map { $0.description }) }
+ .reduce([String: [String]]()) { (refinements, arg1) in
+ let (attribute, values) = arg1
+ return refinements.merging([attribute: values], uniquingKeysWith: { (_, new) -> [String] in
+ new
+ })
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/FilterState/HierarchicalManageable.swift b/Sources/InstantSearchCore/FilterState/HierarchicalManageable.swift
new file mode 100644
index 00000000..b03f1400
--- /dev/null
+++ b/Sources/InstantSearchCore/FilterState/HierarchicalManageable.swift
@@ -0,0 +1,18 @@
+//
+// HierarchicalManageable.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 16/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol HierarchicalManageable {
+
+ func hierarchicalAttributes(forGroupWithName groupName: String) -> [Attribute]
+ func hierarchicalFilters(forGroupWithName groupName: String) -> [Filter.Facet]
+ mutating func set(_ hierarchicalAttributes: [Attribute], forGroupWithName groupName: String)
+ mutating func set(_ hierarchicalFilters: [Filter.Facet], forGroupWithName groupName: String)
+
+}
diff --git a/Sources/InstantSearchCore/Helper/UserAgentSetter.swift b/Sources/InstantSearchCore/Helper/UserAgentSetter.swift
new file mode 100644
index 00000000..8e24934c
--- /dev/null
+++ b/Sources/InstantSearchCore/Helper/UserAgentSetter.swift
@@ -0,0 +1,19 @@
+//
+// UserAgentSetter.swift
+//
+//
+// Created by Vladislav Fitc on 16/06/2020.
+//
+
+import Foundation
+
+struct UserAgentSetter {
+
+ /// Add the library's version to the client's user agents, if not already present.
+ static let set = Self()
+
+ init() {
+ UserAgentController.append(userAgent: .init(title: "InstantSearch iOS", version: Version.current.description))
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Helper/Version+Current.swift b/Sources/InstantSearchCore/Helper/Version+Current.swift
new file mode 100644
index 00000000..e2421ab3
--- /dev/null
+++ b/Sources/InstantSearchCore/Helper/Version+Current.swift
@@ -0,0 +1,2 @@
+// This is generated file. Don't modify it manually.
+public extension Version { static let current: Version = .init(major: 7, minor: 0, patch: 0, prereleaseIdentifier: nil) }
diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalConnector.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalConnector.swift
new file mode 100644
index 00000000..d9795cd1
--- /dev/null
+++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalConnector.swift
@@ -0,0 +1,42 @@
+//
+// HierarchicalConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 28/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class HierarchicalConnector: Connection {
+
+ public let searcher: SingleIndexSearcher
+ public let filterState: FilterState
+ public let interactor: HierarchicalInteractor
+
+ public let searcherConnection: Connection
+ public let filterStateConnection: Connection
+
+ public init(searcher: SingleIndexSearcher,
+ attribute: Attribute,
+ filterState: FilterState,
+ hierarchicalAttributes: [Attribute],
+ separator: String) {
+ self.searcher = searcher
+ self.filterState = filterState
+ self.interactor = HierarchicalInteractor(hierarchicalAttributes: hierarchicalAttributes, separator: separator)
+ self.searcherConnection = interactor.connectSearcher(searcher: searcher)
+ self.filterStateConnection = interactor.connectFilterState(filterState)
+ }
+
+ public func connect() {
+ searcherConnection.connect()
+ filterStateConnection.connect()
+ }
+
+ public func disconnect() {
+ searcherConnection.disconnect()
+ filterStateConnection.disconnect()
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalController.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalController.swift
new file mode 100644
index 00000000..9131d195
--- /dev/null
+++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalController.swift
@@ -0,0 +1,15 @@
+//
+// HierarchicalController.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 08/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol HierarchicalController: ItemController {
+
+ var onClick: ((String) -> Void)? { get set }
+
+}
diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Controller.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Controller.swift
new file mode 100644
index 00000000..7b5c6a65
--- /dev/null
+++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Controller.swift
@@ -0,0 +1,68 @@
+//
+// HierarchicalInteractor+Controller.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 08/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias HierarchicalFacet = (facet: Facet, level: Int, isSelected: Bool)
+
+public extension Hierarchical {
+
+ struct ControllerConnection: Connection where Output == Controller.Item {
+
+ public let interactor: HierarchicalInteractor
+ public let controller: Controller
+ public let presenter: ([HierarchicalFacet]) -> Output
+
+ public func connect() {
+ let presenter = self.presenter
+ interactor.onItemChanged.subscribePast(with: controller) { [weak interactor] controller, facets in
+ guard let interactor = interactor else { return }
+
+ let hierarchicalFacets = facets.enumerated()
+ .map { index, items in
+ items.map { item in
+ (item, index, interactor.selections.contains(item.value))
+ }
+ }.flatMap { $0 }
+
+ controller.setItem(presenter(hierarchicalFacets))
+ }.onQueue(.main)
+
+ controller.onClick = interactor.computeSelection(key:)
+ }
+
+ public func disconnect() {
+ interactor.onItemChanged.cancelSubscription(for: controller)
+ controller.onClick = nil
+ }
+
+ }
+
+}
+
+public extension HierarchicalInteractor {
+
+ @discardableResult func connectController(_ controller: Controller,
+ presenter: @escaping ([HierarchicalFacet]) -> Output) -> Hierarchical.ControllerConnection {
+ let connection = Hierarchical.ControllerConnection(interactor: self, controller: controller, presenter: presenter)
+ connection.connect()
+ return connection
+ }
+
+}
+
+public extension HierarchicalInteractor {
+
+ @discardableResult func connectController(_ controller: Controller,
+ presenter: @escaping HierarchicalPresenter = DefaultPresenter.Hierarchical.present) -> Hierarchical.ControllerConnection {
+ let connection = Hierarchical.ControllerConnection(interactor: self, controller: controller, presenter: presenter)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+FilterState.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+FilterState.swift
new file mode 100644
index 00000000..2cf17b08
--- /dev/null
+++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+FilterState.swift
@@ -0,0 +1,64 @@
+//
+// HierarchicalInteractor+FilterState.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 03/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension HierarchicalInteractor {
+
+ struct FilterStateConnection: Connection {
+
+ public let interactor: HierarchicalInteractor
+ public let filterState: FilterState
+
+ public func connect() {
+
+ let groupName = "_hierarchical"
+
+ filterState[hierarchical: groupName].set(interactor.hierarchicalAttributes)
+
+ interactor.onSelectionsComputed.subscribePast(with: filterState) { [weak interactor] filterState, selections in
+
+ interactor?.selections = selections.map { $0.value.description }
+
+ filterState[hierarchical: groupName].removeAll()
+
+ guard let lastSelectedFilter = selections.last else {
+ filterState[hierarchical: groupName].set([Filter.Facet]())
+ return
+ }
+
+ filterState[hierarchical: groupName].add(lastSelectedFilter)
+ filterState[hierarchical: groupName].set(selections)
+ filterState.notifyChange()
+
+ }
+
+ filterState.onChange.subscribePast(with: interactor) { _, _ in
+ // TODO
+ }
+
+ }
+
+ public func disconnect() {
+ interactor.onSelectionsChanged.cancelSubscription(for: filterState)
+ filterState.onChange.cancelSubscription(for: interactor)
+ }
+
+ }
+
+}
+
+public extension HierarchicalInteractor {
+
+ @discardableResult func connectFilterState(_ filterState: FilterState) -> FilterStateConnection {
+ let connection = FilterStateConnection(interactor: self, filterState: filterState)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Searcher.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Searcher.swift
new file mode 100644
index 00000000..5642e804
--- /dev/null
+++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Searcher.swift
@@ -0,0 +1,53 @@
+//
+// HierarchicalInteractor+Searcher.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 03/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+import AlgoliaSearchClient
+public extension HierarchicalInteractor {
+
+ struct SingleIndexSearcherConnection: Connection {
+
+ public let interactor: HierarchicalInteractor
+ public let searcher: SingleIndexSearcher
+
+ public func connect() {
+
+ for attribute in interactor.hierarchicalAttributes {
+ searcher.indexQueryState.query.updateQueryFacets(with: attribute)
+ }
+
+ searcher.onResults.subscribePast(with: interactor) { interactor, searchResults in
+
+ if let hierarchicalFacets = searchResults.hierarchicalFacets {
+ interactor.item = interactor.hierarchicalAttributes.map { hierarchicalFacets[$0] }.compactMap { $0 }
+ } else if let firstHierarchicalAttribute = interactor.hierarchicalAttributes.first {
+ interactor.item = searchResults.facets?[firstHierarchicalAttribute].flatMap { [$0] } ?? []
+ } else {
+ interactor.item = []
+ }
+ }
+
+ }
+
+ public func disconnect() {
+ searcher.onResults.cancelSubscription(for: interactor)
+ }
+
+ }
+
+}
+
+public extension HierarchicalInteractor {
+
+ @discardableResult func connectSearcher(searcher: SingleIndexSearcher) -> SingleIndexSearcherConnection {
+ let connection = SingleIndexSearcherConnection(interactor: self, searcher: searcher)
+ connection.connect()
+ return connection
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor.swift
new file mode 100644
index 00000000..25657d17
--- /dev/null
+++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor.swift
@@ -0,0 +1,58 @@
+//
+// HiearchicalInteractor.swift
+// InstantSearchCore
+//
+// Created by Guy Daher on 03/07/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public typealias HierarchicalPath = [Filter.Facet]
+
+public class HierarchicalInteractor: ItemInteractor<[[Facet]]> {
+
+ let hierarchicalAttributes: [Attribute]
+ let separator: String
+
+ // Might be a string?
+ public var selections: [String] {
+ didSet {
+ if oldValue != selections {
+ onSelectionsChanged.fire(selections)
+ }
+ }
+ }
+
+ public let onSelectionsChanged: Observer<[String]>
+ public let onSelectionsComputed: Observer
+
+ public init(hierarchicalAttributes: [Attribute], separator: String) {
+ self.hierarchicalAttributes = hierarchicalAttributes
+ self.separator = separator
+ self.onSelectionsChanged = .init()
+ self.onSelectionsComputed = .init()
+ self.selections = []
+ super.init(item: [])
+
+ }
+
+ public func computeSelection(key: String) {
+ let selections = key.subPaths(withSeparator: separator)
+ let hierarchicalPath = zip(hierarchicalAttributes, selections).map { Filter.Facet(attribute: $0, stringValue: $1) }
+ onSelectionsComputed.fire(hierarchicalPath)
+ }
+}
+
+public enum Hierarchical {}
+
+extension String {
+
+ func subPaths(withSeparator separator: String) -> [String] {
+ return components(separatedBy: separator).reduce([]) { (paths, component) in
+ let newPath = paths.last.flatMap { $0 + separator + component } ?? component
+ return paths + [newPath]
+ }
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Highlighting/NSAttributedString+TaggedString.swift b/Sources/InstantSearchCore/Highlighting/NSAttributedString+TaggedString.swift
new file mode 100644
index 00000000..e0ac7588
--- /dev/null
+++ b/Sources/InstantSearchCore/Highlighting/NSAttributedString+TaggedString.swift
@@ -0,0 +1,79 @@
+//
+// NSAttributedString+TaggedString.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 14/06/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public extension Hit {
+
+ /// Returns a highlighted string for a string key if highlightResult has a flat dictionary structure
+ /// If the value for key is missing or it is an embedded structure, returns nil
+ func hightlightedString(forKey key: String) -> HighlightedString? {
+ return highlightResult?.value(forKey: key)?.value?.value
+ }
+
+}
+
+extension NSAttributedString {
+
+ public convenience init(taggedString: TaggedString,
+ inverted: Bool = false,
+ attributes: [NSAttributedString.Key: Any]) {
+ let attributedString = NSMutableAttributedString(string: taggedString.output)
+ let ranges = inverted ? taggedString.untaggedRanges : taggedString.taggedRanges
+ ranges.forEach { range in
+ attributedString.addAttributes(attributes, range: NSRange(range, in: taggedString.output))
+ }
+ self.init(attributedString: attributedString)
+ }
+
+ public convenience init(highlightedString: HighlightedString,
+ inverted: Bool = false,
+ attributes: [NSAttributedString.Key: Any]) {
+ self.init(taggedString: highlightedString.taggedString, inverted: inverted, attributes: attributes)
+ }
+
+ public convenience init(highlightResult: HighlightResult,
+ inverted: Bool = false,
+ attributes: [NSAttributedString.Key: Any]) {
+ self.init(taggedString: highlightResult.value.taggedString, inverted: inverted, attributes: attributes)
+ }
+
+ public convenience init(taggedStrings: [TaggedString],
+ inverted: Bool = false,
+ separator: NSAttributedString,
+ attributes: [NSAttributedString.Key: Any]) {
+
+ let resultString = NSMutableAttributedString()
+
+ for (idx, taggedString) in taggedStrings.enumerated() {
+
+ let substring = NSAttributedString(taggedString: taggedString, inverted: inverted, attributes: attributes)
+ resultString.append(substring)
+
+ // No need to add separator if joined last substring
+ if idx != taggedStrings.endIndex - 1 {
+ resultString.append(separator)
+ }
+ }
+
+ self.init(attributedString: resultString)
+
+ }
+
+ public convenience init(highlightedResults: [HighlightResult],
+ inverted: Bool = false,
+ separator: NSAttributedString,
+ attributes: [NSAttributedString.Key: Any]) {
+ let taggedStrings = highlightedResults.map { $0.value.taggedString }
+ self.init(taggedStrings: taggedStrings,
+ inverted: inverted,
+ separator: separator,
+ attributes: attributes)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift
new file mode 100644
index 00000000..feb7c480
--- /dev/null
+++ b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift
@@ -0,0 +1,51 @@
+//
+// AnyHitsInteractor.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 15/03/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+import Foundation
+import AlgoliaSearchClient
+/** This is a type-erasure protocol for HitsInteractor which makes possible
+ to create a collections of hits interactors with different record types.
+*/
+
+public protocol AnyHitsInteractor: class {
+
+ var onError: Observer { get }
+
+ var pageLoader: PageLoadable? { get set }
+
+ /// Updates search results with a search results with a hit of JSON type.
+ /// Internally it tries to convert JSON to a record type of hits Interactor
+ /// - Parameter searchResults:
+ /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits Interactor
+
+ @discardableResult func update(_ searchResults: HitsExtractable & SearchStatsConvertible) -> Operation
+
+ /// Returns a hit for row of a desired type
+ /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits Interactor
+
+ func genericHitAtIndex(_ index: Int) throws -> R?
+
+ /// Returns a hit for row as dictionary
+
+ func rawHitAtIndex(_ index: Int) -> [String: Any]?
+
+ /// Returns number of hits
+ func numberOfHits() -> Int
+
+ /// Returns currently stored hits of a desired type
+ /// This method doesn't trigger pages loading for infinite scrolling
+ /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits Interactor
+ func getCurrentGenericHits() throws -> [R]
+
+ /// Returns currently stored raw hits
+ /// This method doesn't trigger pages loading for infinite scrolling
+ func getCurrentRawHits() -> [[String: Any]]
+
+ func notifyQueryChanged()
+ func process(_ error: Swift.Error, for query: Query)
+
+}
diff --git a/Sources/InstantSearchCore/Hits/GeoHitsController.swift b/Sources/InstantSearchCore/Hits/GeoHitsController.swift
new file mode 100644
index 00000000..dd6083a2
--- /dev/null
+++ b/Sources/InstantSearchCore/Hits/GeoHitsController.swift
@@ -0,0 +1,17 @@
+//
+// GeoHitsController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 02/10/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol GeoHitsController: class, Reloadable {
+
+ associatedtype DataSource: HitsSource where DataSource.Record: Geolocated
+
+ var hitsSource: DataSource? { get set }
+
+}
diff --git a/Sources/InstantSearchCore/Hits/HitsConnector.swift b/Sources/InstantSearchCore/Hits/HitsConnector.swift
new file mode 100644
index 00000000..63247036
--- /dev/null
+++ b/Sources/InstantSearchCore/Hits/HitsConnector.swift
@@ -0,0 +1,94 @@
+//
+// HitsConnector.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 29/11/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public class HitsConnector: Connection {
+
+ public let searcher: Searcher
+ public let interactor: HitsInteractor
+ public let filterState: FilterState?
+
+ public let filterStateConnection: Connection?
+ public let searcherConnection: Connection
+
+ internal init(searcher: S,
+ interactor: HitsInteractor,
+ filterState: FilterState? = .none,
+ connectSearcher: (S) -> Connection) {
+ self.searcher = searcher
+ self.filterState = filterState
+ self.interactor = interactor
+ self.filterStateConnection = filterState.flatMap(interactor.connectFilterState)
+ self.searcherConnection = connectSearcher(searcher)
+ }
+
+ public func connect() {
+ filterStateConnection?.connect()
+ searcherConnection.connect()
+ }
+
+ public func disconnect() {
+ filterStateConnection?.disconnect()
+ searcherConnection.disconnect()
+ }
+
+}
+
+public extension HitsConnector {
+
+ convenience init(searcher: SingleIndexSearcher,
+ interactor: HitsInteractor,
+ filterState: FilterState? = .none) {
+ self.init(searcher: searcher,
+ interactor: interactor,
+ filterState: filterState,
+ connectSearcher: interactor.connectSearcher)
+ }
+
+ convenience init(appID: ApplicationID,
+ apiKey: APIKey,
+ indexName: IndexName,
+ interactor: HitsInteractor,
+ filterState: FilterState? = .none) {
+ let searcher = SingleIndexSearcher(appID: appID,
+ apiKey: apiKey,
+ indexName: indexName)
+ self.init(searcher: searcher,
+ interactor: interactor,
+ filterState: filterState,
+ connectSearcher: interactor.connectSearcher)
+ }
+
+}
+
+public typealias PlaceHit = Hit
+
+public extension HitsConnector where Hit == PlaceHit {
+
+ convenience init(searcher: PlacesSearcher,
+ interactor: HitsInteractor,
+ filterState: FilterState? = .none) {
+ self.init(searcher: searcher,
+ interactor: interactor,
+ filterState: filterState,
+ connectSearcher: interactor.connectPlacesSearcher)
+ }
+
+ convenience init(placesAppID: ApplicationID,
+ apiKey: APIKey,
+ interactor: HitsInteractor,
+ filterState: FilterState? = .none) {
+ let searcher = PlacesSearcher(appID: placesAppID, apiKey: apiKey)
+ self.init(searcher: searcher,
+ interactor: interactor,
+ filterState: filterState,
+ connectSearcher: interactor.connectPlacesSearcher)
+ }
+
+}
diff --git a/Sources/InstantSearchCore/Hits/HitsController.swift b/Sources/InstantSearchCore/Hits/HitsController.swift
new file mode 100644
index 00000000..df3140f7
--- /dev/null
+++ b/Sources/InstantSearchCore/Hits/HitsController.swift
@@ -0,0 +1,19 @@
+//
+// HitsController.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 23/05/2019.
+// Copyright © 2019 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol HitsController: class, Reloadable {
+
+ associatedtype DataSource: HitsSource
+
+ var hitsSource: DataSource? { get set }
+
+ func scrollToTop()
+
+}
diff --git a/Sources/InstantSearchCore/Hits/HitsExtractable.swift b/Sources/InstantSearchCore/Hits/HitsExtractable.swift
new file mode 100644
index 00000000..4d7f096a
--- /dev/null
+++ b/Sources/InstantSearchCore/Hits/HitsExtractable.swift
@@ -0,0 +1,35 @@
+//
+// HitsResponse.swift
+// InstantSearchCore
+//
+// Created by Vladislav Fitc on 15/04/2020.
+// Copyright © 2020 Algolia. All rights reserved.
+//
+
+import Foundation
+
+public protocol HitsExtractable {
+
+ func extractHits() throws -> [T]
+
+}
+
+extension SearchResponse: HitsExtractable {}
+
+extension PlacesResponse: HitsExtractable {
+
+ public func extractHits