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.

findsimilar-example.gif

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:

  1. 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.

  2. 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
    
  3. In your application target's “General” settings tab, in the Embedded Binary section, drag and drop the following frameworks from the Carthage/Build/iOS folder:

    • Kingfisher.framework
    • LayoutKit.framework
    • ViSearchSDK.framework
    • ViSearchWidgets.framework

    add_frameworks

    Click on "Build Phases" tab, verify that the "Framework Search Path" includes $(PROJECT_DIR)/Carthage/Build/iOS

  4. Add the following frameworks to "Linked Frameworks and Libraries" section: MediaPlayer, Photos, AVFoundation.

  5. 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
    

    build_script

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.

    privacy

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.

product_card

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:

The screenshot below is using Search by Color as an example, however the sample code for creating filters can be applied to all widgets. filter

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

Reference

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.

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.

app_key

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.

product_card 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.

schema_mapping

  • heading_schema_mapping : Refers to the schema mapping for the Heading field in the Product Card component. In the screenshot, it was used to display the product title (the schema field is im_title which is a custom field in the feed).
  • label_schema_mapping : Refers to the schema mapping for the Label field in the Product Card component. In the screenshot, it was used to display the product brand.
  • price_schema_mapping : Refers to the schema mapping for the Price field in the Product 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 the Discount Price field in the Product 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.

scheme

Reference

We listed references used for this widget below.

Alternatively, you may want to checkout out the complete API docs.

Find Similar Widget

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 the limit 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()
    ...