Requirements

The Elements iOS Flexible UI SDK a.k.a Treasure SDK requires Xcode 11 or later and is compatible with apps targeting iOS 13 or above.

Installation

Flexible UI SDK for iOS are available through CocoaPods.

CocoaPods

Add pod 'TreasureUI' to your Podfile.
Run pod install.

Configuration

TreasureHost

TreasureHost class is the main interface that communicates between client application and flexible UI SDK. You can initialize TreasureHost in the following way.

import Treasure

let clientToken = "Your elements client token goes here..."
let env = TreasureEnvironment.sandbox(clientToken: clientToken) // Specify your client environment here.
let treasureHost = TreasureHost(configuration: .init(environment: env))

Set up client side dependencies

Treasure provides interfaces to support passing client dependencies to itself.

Load Images

Treasure allows you to pass customized image downloader to the SDK where Treasure will use it to load images when needed.

See code below for an example on how to create a customized image loader using popular image loading libraries -> SDWebImage + Lottie

import Lottie
import SDWebImage
import Treasure
import UIKit

// Any customized image loader needs to implement TreasureImageLoadable interface.
public final class DefaultTreasureImageLoader: TreasureImageLoadable {

  public init() {}

  public func setImage(
    imageView: UIImageView,
    placeHolder: UIImage?,
    url: URL,
    completion: (() -> Void)?
  ) {
    imageView.sd_setImage(with: url, placeholderImage: placeHolder, completed: { image, _, _, _ in
      completion?()
    })
  }

  public func createAnimationView(
    url: URL,
    imageMode: UIView.ContentMode,
    completion: ((Error?) -> Void)?
  ) -> UIView {
    let lottieView = AnimationView(url: url, closure: { error in
      if let error = error {
        completion?(error)
        return
      }
      completion?(nil)
    })
    setupAnimationViewCommonProps(view: lottieView, imageMode: imageMode)
    return lottieView
  }

  public func createAnimationView(
    animationName: String,
    imageMode: UIView.ContentMode
  ) -> UIView {
    let lottieView = AnimationView(name: animationName)
    setupAnimationViewCommonProps(view: lottieView, imageMode: imageMode)
    return lottieView
  }

  public func playAnimationImage(view: UIView) {
    guard let lottieView = view as? AnimationView else { return }
    lottieView.play(completion: nil)
  }

  private func setupAnimationViewCommonProps(
    view: AnimationView,
    imageMode: UIView.ContentMode
  ) {
    view.contentMode = imageMode
    view.loopMode = .loop
    view.backgroundColor = .clear
    view.layer.masksToBounds = true
  }

  public func getImage(url: URL, completion: ((UIImage?) -> Void)?) {
    SDWebImageManager.shared.loadImage(with: url, options: [], progress: nil) { image, data, error, cachType, success, url in
      completion?(image)
    }
  }
}

Now you can pass the customized image loader to TreasureDependency in the following way.

TreasureDependency.shared.imageLoader = DefaultTreasureImageLoader()

Theme + UI resources

Treasure supports dark/light themes at the moment. Client side colors/fonts/image can be passed to TreasureDependency using ThemeComponent interface.

Fonts

Treasure supports two different ways of constructing UIFont objects.

  1. Constructing using font name and size
"text_font": {
  "name": "Poppins-Medium",
  "size": 18
}
  1. Constructing font using string representation provided by client side application.
"text_font": "primary"

Example code to provide multiple fonts to Treasure.

enum FontPlate: String, CaseIterable {
  case primary
  case secondary

  var toDarkThemeFont: UIFont {
    switch self {
    case .primary:
      return UIFont.systemFont(ofSize: 30)
    case .secondary:
      return UIFont.systemFont(ofSize: 20)
    }
  }

  var toLightThemeFont: UIFont {
    switch self {
    case .primary:
      return UIFont.systemFont(ofSize: 30)
    case .secondary:
      return UIFont.systemFont(ofSize: 20)
    }
  }

  static var toFonts: ThemeComponent<UIFont> {
    var result: [String: [SystemThemeType: UIFont]] = [:]
    for type in FontPlate.allCases {
      result[type.rawValue] = [
        .dark: type.toDarkThemeFont,
        .light: type.toLightThemeFont
      ]
    }
    return .init(values: result)
  }
}

So once Treasure SDK sees "primary" in fonts json block, it will be mapped to the primary font provided by the application.

Colors

Similar to fonts, Treasure currently supports two different ways of initializing colors.

  1. Using hex string
"text_color": "#000000" // this represents a black text color
  1. Using color string representation provided by the application.
"text_color": "primary"

Example code to provide customized colors to Treasure.

import Treasure
import UIKit

enum ColorPlate: String, CaseIterable {
  case primary
  case secondary

  var toDarkThemeColor: UIColor {
    switch self {
    case .primary:
      return .white
    case .secondary:
      return .white
    }
  }

  var toLightThemeColor: UIColor {
    switch self {
    case .primary:
      return .darkGray
    case .secondary:
      return .red
    }
  }

  static var toColors: ThemeComponent<UIColor> {
    var result: [String: [SystemThemeType: UIColor]] = [:]
    for type in ColorPlate.allCases {
      result[type.rawValue] = [
        .dark: type.toDarkThemeColor,
        .light: type.toLightThemeColor
      ]
    }
    return .init(values: result)
  }
}

Images

Treasure provides initializing images from local assets folder and remote download URL. In order to use images from local assets folder you can do the following.

import Treasure
import UIKit

enum LocalImages: String, CaseIterable {
  case logo

  var toDarkThemeImage: UIImage {
    UIImage(named: rawValue) ?? .init()
  }

  var toLightThemeImage: UIImage {
    UIImage(named: rawValue) ?? .init()
  }

  static var toImages: ThemeComponent<UIImage> {
    var result: [String: [SystemThemeType: UIImage]] = [:]
    for type in LocalImages.allCases {
      result[type.rawValue] = [
        .dark: type.toDarkThemeImage,
        .light: type.toLightThemeImage
      ]
    }
    return .init(values: result)
  }
}

Styles

Treasure also supports passing code level style configs as dependencies to the SDK.

import Foundation
import Treasure
import UIKit

enum StylesPlate: String, CaseIterable {
  case primaryLabel = "primary_label"
  case navBarImage = "image_nav_bar"
  case navBarStyle = "nav_bar_style"
  case horizontalDivider = "horizontal_divider"

  var toStyle: ElementStyle {
    switch self {
    case .primaryLabel:
      return LabelElementStyle(
        id: rawValue,
        basicProps: .init(layoutMargins: .init(values: [12, 24, 12, 12])),
        textColor: .designSystem(ColorPlate.primary.rawValue),
        textFont: .designSystem(FontPlate.primary.rawValue),
        alignment: .leading,
        numberOfLines: 1
      )
    case .navBarImage:
      return ImageElementStyle(
        id: rawValue,
        basicProps: .init(layoutMargins: .init(values: [0, 0, 0, 0])),
        imageHeight: 20,
        imageWidth: 20
      )
    case .navBarStyle:
      return NavBarElementStyle(
        id: rawValue,
        basicProps: .init(
          backgroundColor: "#F2F2F2",
          layoutMargins: .init(values: [12, 20, 12, 12])
        ),
        titleStyleId: StylesPlate.primaryLabel.rawValue,
        imageStyleId: StylesPlate.navBarImage.rawValue,
        bottomLineColor: "#CCCCCC",
        bottomLineHeight: 1,
        bottomLineVisibility: .visibleByContentOffset
      )
    case .horizontalDivider:
      return DividerElementStyle(
        id: rawValue,
        height: 1,
        color: "#ff5647"
      )
    }
  }

  static var toStyles: [String: ElementStyle] {
    var result: [String: ElementStyle] = [:]
    for type in StylesPlate.allCases {
      result[type.rawValue] = type.toStyle
    }
    return result
  }
}

So when Treasure sees "primary_label" in the json style file, it will default to use the primaryLabel style defined in the style plate.

Constructing theme object from fonts, colors, images and styles.

In order for treasure to use provided resources, you need to construct an object that implements ThemeRegistrable interface and then pass that object to TreasureDependency.

import Treasure
import UIKit

public struct Theme: ThemeRegistrable {
  public var styles: [String: ElementStyle]?
  public var fonts: ThemeComponent<UIFont>?
  public var colors: ThemeComponent<UIColor>?
  public var images: ThemeComponent<UIImage>?
}

TreasureDependency.shared.theme = Theme(
  styles: StylesPlate.toStyles,
  fonts: FontPlate.toFonts,
  colors: ColorPlate.toColors,
  images: LocalImages.toImages
)

External action handler

Treasure provided delegation to handle ExternalAction

final class PaymentFlowViewController: UIViewControlelr {
  
  init() {
    ...
    super.init(nibName: nil, bundle: nil)
    ...
    treasureHost.hostActionHandler = self
  }
}

extension PaymentFlowViewController: TreasureHostActionHandler {
  
  func handle(externalAction: ExternalAction) {
    let vc = UIViewController()
    vc.view.backgroundColor = .red
    treasureHost.pushViewController(vc: vc)
  }
}

Starting Treasure UI

Method 1: Load from Elements' server.

func loadFromCheckoutUI() {
  // This method will return a UIViewController on completion, you can either
  // replace the current screen with this view controller or push/present this
  // view controller.
  treasureHost.loadFromCheckoutUI { [weak self] vc in
    self?.replaceScreen(viewController: vc)
  }
}

Method 2: Load from local file

func loadFromLocal() {
  let filePath = "/Users/elements/Desktop/Treasure/TreasureExample/TreasureExample/Resources/blocks_ui.json"
  let vc = treasureHost.loadFromLocal(filePath: filePath)
  replaceScreen(viewController: vc)
}

once you run your application on a simulator you will get the live update feature when you modify your local json file.

Example App

Clone this repo and run pod install and then open TreasureUIExample.xcworkspace. The demo app demonstrated how to use Treasure .