Swift Doc
Current version: 1.5.0
Visually similar products can be actively searched by the user on the product listing or product detail screen. Our algorithm assigns appropriate weights to different attributes to determine a final similarity score, and product results are displayed in order of the score. This solution provides an opportunity for shoppers to discover other relevant results based on visual similarity.
The products are displayed in a grid (collection view).
Follow the steps below to integrate this widget into your project.
If you want to try out the solution widgets before integration, we have prepared a demo for you.
Step 1. Get the required version of Xcode
To include ViSearch Widget SDK in your project, the software prerequisites are:
- Recommended: iOS 9.0+ . Minimum: iOS 8.0+.
- iOS 9.0+: The SDK has been tested on physical iPhone device hardware.
- iOS 8.x: The SDK has only been tested on emulators.
- Xcode 8.1+
- Swift 3.0+
Step 2. Install the SDK
CocoaPods
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
sudo gem install cocoapods
CocoaPods 1.1.0+ is required to build ViSearchWidgets.
Go to your Xcode project directory to create an empty Podfile:
pod init
To integrate ViSearchWidgets into your Xcode project using CocoaPods, specify it in your Podfile
:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
pod 'ViSearchWidgets', '~> 0.1'
end
You should change version 0.1 to the latest version of ViSearchWidgets. The version numbers can be viewed under the current Github project tags.
Then, run the following command:
pod install
Carthage
Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with Homebrew using the following command:
brew update
brew install carthage
Alternately, you can download and run the Carthage.pkg
file for the latest release.
To integrate ViSearchWidgets into your Xcode project using Carthage:
Create a
Cartfile
, and add contents below to it:github "visenze/visearch-widget-swift" ~> 0.1
You should change version 0.1 to the latest version of ViSearchWidgets. The version numbers can be viewed under the current Github project tags.
Use the following command to fetch dependencies (Kingfisher, LayoutKit, visearch-sdk-swift, visearch-widget-swift) into
Carthage/Checkouts
folder, then build the framework.carthage update --platform iOS --no-use-binaries
In your application target's “General” settings tab, in the
Embedded Binary
section, drag and drop the following frameworks from theCarthage/Build/iOS
folder:- Kingfisher.framework
- LayoutKit.framework
- ViSearchSDK.framework
- ViSearchWidgets.framework
Click on "Build Phases" tab, verify that the "Framework Search Path" includes
$(PROJECT_DIR)/Carthage/Build/iOS
Add the following frameworks to "Linked Frameworks and Libraries" section: MediaPlayer, Photos, AVFoundation.
On your application target’s “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase”. Create a Run Script in which you specify your shell (ex:
bin/sh
), add the following contents to the script area below the shell:/usr/local/bin/carthage copy-frameworks
and add the paths to the frameworks you want to use under
Input Files
, e.g.:$(SRCROOT)/Carthage/Build/iOS/Kingfisher.framework $(SRCROOT)/Carthage/Build/iOS/LayoutKit.framework $(SRCROOT)/Carthage/Build/iOS/ViSearchSDK.framework $(SRCROOT)/Carthage/Build/iOS/ViSearchWidgets.framework
Step 3. Configure the SDK
APP Key
ViSearch
must be initialized with an App Key before it can be used.
Please refer to access your APP key section for instructions to get the APP key.
You can setup the APP key in AppDelegate class.
import ViSearchSDK
import ViSearchWidgets
...
// init ViSearch client with app key
ViSearch.sharedInstance.setup(appKey: "YOUR_APP_KEY")
App Permission
App Transport Security Setting : For loading of product images, you will need to configure the "App Transport Security Settings" option in your project's
Info.plist
. Please see this link and suggestions for more information. If your product image URLs come from various unknown domains, you can just set "Arbitrary Load" option to "Yes".Add Privacy Usage Description : iOS 10 now requires user permission to access camera and photo library. To use "Search by Image" solution, please add descriptions for NSCameraUsageDescription, NSPhotoLibraryUsageDescription for accessing camera/photo library respectively in your
Info.plist
. More details can be found here.
Step 4. Add the Widget
Widget Configuration
All of our widgets (constructed as view controller sub-classes of ViBaseSearchViewController require the following common configuration steps.
Product Card Schema Mapping
This section will describe how to display your schema fields in the product cards via schema mapping. For instructions to upload your data feed and configure the schema fields, please refer to configure schema fields.
The fields which hold the products' information can then be displayed in the widgets via the Product Card
UI component.
Please see the below screenshot for an example.
You can then configure the widgets (which are view controllers) as follows:
// Create the widgets as view controller
...
...
// Configure schema mapping for product card UI component in the widget
controller.schemaMapping.heading = ... // Mapping for heading element e.g. for displaying product title
controller.schemaMapping.label = ... // Mapping for label element e.g. for displaying product brand
controller.schemaMapping.price = ... // Mapping for price element e.g. for displaying product original retail price
controller.schemaMapping.discountPrice = ... // Mapping for discount price element e.g. for displaying product discount price. May not be available in product feed
controller.schemaMapping.productUrl = ... // Mapping for product image URL. default to "im_url" schema field.
Product Card Display Setting
You can also configure various settings for the product card.
// Configure product image size and content mode
controller.imageConfig.size = CGSize(width: imageWidth, height: imageHeight)
controller.imageConfig.contentMode = .scaleAspectFill
// Configure product card box size
controller.itemSize = ...
// Add border to product card
controller.productCardBorderColor = UIColor.lightGray
controller.productCardBorderWidth = 0.7
// Add only bottom and right borders for product card
controller.productBorderStyles = [.RIGHT , .BOTTOM]
// Display a strike through text through the original retail price (if discount price is available)
controller.priceConfig.isStrikeThrough = true
Common Search Settings
// Create search params (depending on the widget)
// for example, in Find Similar and You May Also Like, parameters are constructed by providing im_name:
// let params = ViSearchParams(imName: im_name)
// Set various search settings
// Limit search to return 16 most similar results
params.limit = 16
// Retrieve additional meta-data (in addition to what was mentioned in schema mapping)
params.fl = ["category"]
// Set search parameters
controller.params = params
For advanced configuration of search parameters refer to this link.
Widget Sample
Below is sample code for using Find Similar widget.
Add this sample code into your controller and make sure your controller is embedded in a navigation controller.
So when this sample code is executed, the widget ViFindSimilarViewController
will be displayed through navigation from your controller.
import ViSearchSDK
import ViSearchWidgets
...
// You can trigger the search from a "Similar" button from product details screen
// Alternately, the "Find Similar" search can be triggered in the search results by clicking on "Find Similar" button on a product card (located at bottom right)
if let params = ViSearchParams(imName: "sample_im_name.jpg") {
// 1. Create Find Similar widget
let similarController = ViFindSimilarViewController()
// Configure max of 16 most similar results to return
params.limit = 16
// 2. Set search parameters
similarController.searchParams = params
// 3. Configure schema mapping
// Assumption: your schema data feed include "im_title", "brand", "price" fields which store data for product title, brand and current price
similarController.schemaMapping.heading = "im_title"
similarController.schemaMapping.label = "brand"
similarController.schemaMapping.price = "price"
// 4. Configure product image size and content mode
let containerWidth = self.view.bounds.width
let imageWidth = containerWidth / 2.5
let imageHeight = imageWidth * 1.2
similarController.imageConfig.size = CGSize(width: imageWidth, height: imageHeight)
// Configure image content mode
similarController.imageConfig.contentMode = .scaleAspectFill
// 5. Configure products to display in 2 columns
similarController.itemSize = similarController.estimateItemSize(numOfColumns: 2, containerWidth: containerWidth)
// 6. Misc setting (Optional)
// Configure border color if necessary
similarController.productCardBorderColor = UIColor.lightGray
similarController.productCardBorderWidth = 0.7
// Configure spacing between product cards on the same row i.e. the column spacing
similarController.itemSpacing = 0
// Configure spacing between the rows
similarController.rowSpacing = 0
// 7. Configure delegate to listen for various events such as when user clicks on Action button
similarController.delegate = self
// 8. Open widget with navigation controller
self.navigationController?.pushViewController(similarController, animated: true)
// 9. Trigger web service to ViSenze server
similarController.refreshData()
}
Step 5. Customize the Widget
Filtering
You can configure the filter component for Find Similar, Search by Image and Search by Color widgets search results. Two types of filters are supported:
- Range filter (e.g. for price) (ViFilterItemRange)
- Multi-selection category filter (e.g. for product category, brand) (ViFilterItemCategory)
The screenshot below is using Search by Color as an example, however the sample code for creating filters can be applied to all widgets.
The filters can be created as follows:
var items : [ViFilterItem] = []
// Configure a 'price' range filter item
// Assumption: there is a int/float field named "price" in your schema data feed
let min : Int = 0
let max : Int = 500
let item = ViFilterItemRange(title: "Price Range ($)", schemaMapping: "price", min: min, max: max)
items.append(item)
// Configure a category filter item for 'brand' schema field
// Assumption: there is a string field named 'brand' in your schema
let brandString = "Bobeau,Coco Style,Etro,Marc Jacobs,Sister Jane,Volcom"
let options = brandString.components(separatedBy: ",")
var optionArr : [ViFilterItemCategoryOption] = []
for o in options {
optionArr.append( ViFilterItemCategoryOption(option: o) )
}
let brandFilterItem = ViFilterItemCategory(title: "Brand", schemaMapping: "brand", options: optionArr)
items.append(brandFilterItem)
To display the filters, you just need to set the filterItems property of the widgets:
controller.filterItems = items
Widgets Theme
To customize the color and styles of the widgets and buttons, you can look at the following classes:
ViTheme : Default global configuration for text fonts, button colors, sizes, etc.
You can configure via the ViTheme singleton:
// configure default font ViTheme.sharedInstance.default_font = ...
ViButtonConfig : Default style for buttons
ViLabelConfig : Default style for labels (heading, label, price, discount price)
ViImageConfig : Product image configuration
For specific widget customization (e.g. show/hide buttons, change text/button colors, etc), refer to UI Settings section of ViBaseSearchViewController
.
Advanced
For advanced use cases where you need to create your own widgets or want to modify/extend the product card, please hook into the ViSearchViewControllerDelegate callbacks.
- configureCell(sender:collectionView:indexPath:cell:) : Allows you to configure the product cell before displaying. You can retrieve various product card UI elements by tag in the cell.contentView e.g. cell.contentView.viewWithTag(ViProductCardTag.productImgTag.rawValue) and configure accordingly. The tags are defined here.
- configureLayout(sender:layout:) : If you need to have your own controller which will change the layout for the built in collectionview.
- controllerWillTransition(controller:to size:with coordinator:) : To reconfigure controllers when orientation changes
- willShowSimilarController(sender:controller:collectionView:indexPath:product:) : configure similar controller before display.
- willShowFilterController(sender:controller:) : Configure filter controller before the Filter screen is shown.
- didSelectProduct(sender:collectionView:indexPath:product:) : Product selection notification i.e. user tapped on a product card
- actionBtnTapped(sender:collectionView:indexPath:product:) : Action button tapped notification i.e. user tapped on action button at the top right corner of a product card cell
- similarBtnTapped(sender:collectionView:indexPath:product:) : User tapped on similar button at the bottom right of a product card cell
- searchSuccess(sender:searchType:reqId:products:) : The search is successful
- searchFailed(sender:searchType:err:apiErrors:) : The search failed due to either network errors or ViSenze API errors
Error Handling
There are 2 possible types of errors when using the widgets:
- Errors when trying to call the API e.g. network related errors like offline/broken/time-out Internet connection
- ViSenze search API-related errors e.g mis-configuration of search parameters, invalid API key, API limit exceeded, invalid im_name
By default, the widgets will show a generic error message (i.e. "An error has occurred. Please try again." for all errors). In addition, the widgets will display "No Results Found" for search with no results found.
To display custom error messages to end users, you can hook into ViSearchViewControllerDelegate
and take appropriate actions in searchFailed(sender:searchType:err:apiErrors:) call back.
...
// Set delegate to current view controller
controller.delegate = self
// Turn off default error message display
controller.showDefaultErrMsg = false
// Turn off default no results message display
controller.showNoSearchResultsMsg = false
...
// Sender here is refering to the controller that called this search
// Hook into this to display your custom error view
func searchFailed(sender: AnyObject, searchType: ViSearchType , err: Error?, apiErrors: [String]) {
if let err = err {
// Display network error e.g. with UIAlertController
// Default network error are stored in err.localizedDescription
}
else if apiErrors.count > 0 {
// ViSenze server will return list of error messages in an array
let msg = apiErrors.joined(separator: ",")
// Display message here if necessary
}
// You can create a custom view (UIView) and then call
DispatchQueue.main.async {
var your_custom_view = ...
controller.setMsgView(your_custom_view)
// Display
controller.showMsgView = true
}
}
// Hook into this to display no results found custom view
func searchSuccess( sender: AnyObject, searchType: ViSearchType, reqId: String? , products: [ViProduct])
{
if(self.products.count == 0 ){
DispatchQueue.main.async {
var your_custom_view = ...
controller.setMsgView(your_custom_view)
// Display
controller.showMsgView = true
}
}
}
Alternately, you can subclass the view controller and override displayDefaultErrMsg
, displayNoResultsFoundMsg
to change the messages display.
Color Picker
To implement the action for Color Picker button, you will need to implement the following code:
// Make sure your custom controller implements ViColorPickerDelegate , UIPopoverPresentationControllerDelegate
// UIPopoverPresentationControllerDelegate is needed to display color picker in a popover
class CustomSearchBarViewController: UIViewController, ViColorPickerDelegate, UIPopoverPresentationControllerDelegate{
...
var colorParms: ViColorSearchParams? = nil
// List of colors for the color picker in hex format e.g. e0b0ff, 2abab3
open var colorList: [String] = [
"000000" , "555555" , "9896a4" ,
"034f84" , "00afec" , "98ddde" ,
"00ffff" , "f5977d" , "91a8d0",
"ea148c" , "f53321" , "d66565" ,
"ff00ff" , "a665a7" , "e0b0ff" ,
"f773bd" , "f77866" , "7a2f04" ,
"cc9c33" , "618fca" , "79c753" ,
"228622" , "4987ec" , "2abab3" ,
"ffffff"
]
/// Open color picker view in a popover
///
/// - Parameters:
/// - sender: color picker button
/// - event: button event
public func openColorPicker(sender: UIButton, forEvent event: UIEvent) {
let controller = ViColorPickerModalViewController()
controller.modalPresentationStyle = .popover
controller.delegate = self
controller.colorList = self.colorList
controller.paddingLeft = 8
controller.paddingRight = 8
controller.preferredContentSize = CGSize(width: self.view.bounds.width, height: 300)
if let colorParams = self.colorParms {
controller.selectedColor = colorParams.color
}
if let popoverController = controller.popoverPresentationController {
popoverController.sourceView = sender
popoverController.sourceRect = sender.bounds
popoverController.permittedArrowDirections = UIPopoverArrowDirection.up
popoverController.delegate = self
}
self.present(controller, animated: true, completion: nil)
}
// MARK: UIPopoverPresentationControllerDelegate
// important - this is needed so that a popover will be properly shown instead of fullscreen
public func adaptivePresentationStyle(for controller: UIPresentationController,
traitCollection: UITraitCollection) -> UIModalPresentationStyle{
return .none
}
// MARK: ViColorPickerDelegate
public func didPickColor(sender: ViColorPickerModalViewController, color: String) {
// Set the color params
self.colorParms = ViColorSearchParams(color: color)
// Refresh data
let controller = ViColorSearchViewController()
self.colorParms!.limit = 16
controller.searchParams = self.colorParms
// Configure schema mapping if needed
controller.schemaMapping = ...
// Configure filter
controller.filterItems = ...
let containerWidth = self!.view.bounds.width
// configure product image size
controller.imageConfig.size = ...
controller.itemSize = controller.estimateItemSize(numOfColumns: 2, containerWidth: containerWidth)
self.navigationController?.pushViewController(controller, animated: true)
controller.refreshData()
sender.dismiss(animated: false, completion: nil)
}
}
Camera Button
To implement action for Camera button, copy the code below to trigger the camera search:
// MARK: camera
/// Open camera to take picture
///
/// - Parameters:
/// - sender: camera button
/// - event: button event
public func openCameraView(sender: UIButton, forEvent event: UIEvent) {
let cameraViewController = CameraViewController(croppingEnabled: false, allowsLibraryAccess: true) { [weak self] image, asset in
// See the code in Search by Image solution
// User cancel photo taking
if( image == nil) {
self?.dismiss(animated: true, completion: nil)
return
}
let controller = ViSearchImageViewController()
// Save recent photo
controller.asset = asset
let params = ViUploadSearchParams(image: image!)
params.limit = 16
// Upload higher res image i.e. max 1024
params.img_settings = ViImageSettings(setting: .highQualitySetting)
controller.searchParams = params
controller.croppingEnabled = true
controller.allowsLibraryAccess = true
// Configure your schema mapping
controller.schemaMapping = ...
// Configure filter items if needed
controller.filterItems = ...
// Sample configuration
let containerWidth = self!.view.bounds.width
// Configure product image size
controller.imageConfig.size = ...
controller.imageConfig.contentMode = .scaleAspectFill
controller.itemSize = controller.estimateItemSize(numOfColumns: 2, containerWidth: containerWidth)
// Set to same delegate
controller.delegate = self
self?.navigationController?.pushViewController(controller, animated: true)
controller.refreshData()
self?.dismiss(animated: false, completion: nil)
}
present(cameraViewController, animated: true, completion: nil)
}
Orientation Changes
To handle orientation changes e.g. you want to display 2 columns in portrait but 4 columns in landscape, you can hook into controllerWillTransition method of ViSearchViewControllerDelegate.
func controllerWillTransition(controller: UIViewController , to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { context in
// ViGridSearchViewController is the super class of Find Similar, Search By Color, Search by Image widgets
if controller is ViGridSearchViewController {
// Reconfigure size
self.configureSize(controller: controller as! ViGridSearchViewController)
(controller as? ViGridSearchViewController)?.collectionView?.reloadData()
}
}, completion: { context in
// after rotate
})
}
// Configure controller size during different orientation
public func configureSize(controller: ViGridSearchViewController) {
let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait
let numOfColumns = isPortrait ? 2 : 4
let containerWidth = UIScreen.main.bounds.size.width
let imageWidth = isPortrait ? (UIScreen.main.bounds.size.width / 2.5) : (UIScreen.main.bounds.size.width / 4.5)
let imageHeight = imageWidth * 1.2
controller.imageConfig.size = CGSize(width: imageWidth, height: imageHeight)
controller.itemSpacing = 0
controller.rowSpacing = 0
// this must be called last after setting schema mapping
// the item size is dynamic and depend on schema mapping
// For example, if label is not provided, then the estimated height would be shorter
controller.itemSize = controller.estimateItemSize(numOfColumns: numOfColumns, containerWidth: containerWidth)
}
Timeout Settings
By default, API search requests will timeout after 10 seconds. To change the timeout, you can configure ViSearch client in AppDelegate didFinishLaunchingWithOptions method:
// Setup search client
ViSearch.sharedInstance.setup(appKey: "YOUR_APP_KEY")
// Configure timeout to 30s example. By default timeout is set 10s.
ViSearch.sharedInstance.client?.timeoutInterval = 30
ViSearch.sharedInstance.client?.sessionConfig.timeoutIntervalForRequest = 30
ViSearch.sharedInstance.client?.sessionConfig.timeoutIntervalForResource = 30
ViSearch.sharedInstance.client?.session = URLSession(configuration: (ViSearch.sharedInstance.client?.sessionConfig)!)
// Configure timeout for downloading an image, default is 15s
KingfisherManager.shared.downloader.downloadTimeout = 30
Demo
Download the Demo
Clone our repo to get the source code of the demo application.
git clone https://github.com/visenze/visearch-widget-swift.git
Build the Demo
The demo app is built with Carthage.
Please download and run the Carthage.pkg
file for the latest release.
After Carthage installation, you will need to run the following command at source directory:
carthage update --platform iOS --no-use-binaries
Configure the Demo
Configure APP Key
Please refer to access your APP key section for instructions to get the APP key.
You need to add the App Key into the ViApiKeys.plist
file as the accessKey.
Configure Schema Mapping
Please refer to configure schema fields section for instructions to upload your data feed and configure the schema fields.
The fields which hold the products' information can then be displayed in the widgets via the Product Card
UI component. Please see the below screenshot for an example.
You will need to edit the SampleData.plist
(the file was below ViApiKeys.plist in the Configure APP key
section screenshot) to configure the schema mapping for your sample data feed.
heading_schema_mapping
: Refers to the schema mapping for theHeading
field in theProduct Card
component. In the screenshot, it was used to display the product title (the schema field isim_title
which is a custom field in the feed).label_schema_mapping
: Refers to the schema mapping for theLabel
field in theProduct Card
component. In the screenshot, it was used to display the product brand.price_schema_mapping
: Refers to the schema mapping for thePrice
field in theProduct Card
component. In the screenshot, it was used to display the product original retail price.discount_price_schema_mapping
: Refers to the schema mapping for theDiscount Price
field in theProduct Card
component. In the screenshot, it was used to display the product discount price. This is optional and may not be applicable for your data feed.color
: Sample color code used for "Search by Color" widget demo.find_similar_im_name
: Sample im_name used for "Find Similar" widget demo. You can browse the product images in ViSenze dashboard and used any existing im_name to test. If you use an invalid im_name (i.e. does not exist), a default error message will be shown within the widget.you_may_like_im_name
: Sample im_name used for "You May Also Like" widget demo. You can browse the product images in ViSenze dashboard and use any existing im_name to test. If you use an invalid im_name (i.e. does not exist), a default error message will be shown within the widget.filterItems
: Configure the types of filters used in demo app. Two types of filters are supported (Category and Range filters).
Configure Scheme
At the final step, you will need to change the Running Scheme to "WidgetsExample". You are now ready to run the demo app.
Reference
We listed references used for this widget below.
Alternatively, you may want to checkout out the complete API docs.
Find Similar Widget
- ViFindSimilarViewController : Find Similar widget
- ViGridSearchViewController : Present search results in a grid (collection view)
- ViBaseSearchViewController : Base class for all widgets
- ViSearchViewControllerDelegate : Delegate for widget customization
Customization
Filtering
Widget Theme
Advanced
Error Handling
Known Issues
Displaying a large number of images in search results may result in a crash:
If you configure the search parameter to return a large number of images in an API call, it may cause a system crash after extended/heavy usage. For example, if you set
controller.searchParams?.limit
to 1000 (the absolute limit), keep scrolling to the end of the search results, and trigger continuous searches (e.g. by clicking on Find Similar button), then crashes may then occur. Note that this behavior is generally not observed for typical users who probably will only scroll through the first 50-100 results. It is recommended that you set thelimit
to a small number e.g. 16-100 to save the bandwidth and faster loading. If you want to implement pagination or infinite scrolling for the widget you can increase the page number and re-trigger the search:// For example, when the server side has 60 items, the search operation will return // the first 30 items with page = 1 and limit = 30. By changing the page to 2, // the search will return the last 30 items. ... controller.searchParams?.page = 2; controller.searchParams?.limit = 30; // Refresh data controller.refreshData() ...