Embed a form builder with Swift

Embed a form builder with Swift

A developer-friendly guide to embedding a form builder into your Swift mobile app.

In this guide, we'll show how to embed a form builder with Swift using Joyfill’s Swift form builder SDK, and learn how to render your first form in minutes.

You can think of Joyfill as a sort of Stripe or Twilio, but for forms.

Reference for integrating with the JoyDoc iOS SDK. Note that it's built using Swift w/ UIKit. So if you are using SwiftUI you will need to wrap the file per Apple Developer Guides suggest (we have included an example as well).

Setup

Please see our Overview and Getting Started Guide

Requirements and Dependencies

View Package README

Installation

Swift Package Manager

  1. To integrate using Swift Package Manager, Swift version >= 5.3 is required.
  2. In your Xcode project from the Project Navigator (Xcode ❯ View ❯ Navigators ❯ Project ⌘ 1) select your project, activate the Package Dependencies tab and click on the plus symbol ➕ to open the Add Package popup window:
  3. Enter the JoyDoc package URL github.com/joyfill/components-ios/tree/main into the search bar in the top right corner of the Add Package popup window.
  4. Select components-ios package
  5. Choose your Dependency Rule (we recommend Up to Next Major Version).
  6. Select the project to which you would like to add JoyDoc, then click Add Package
  7. Select your application target and ensure that under Frameworks, Libraries, and Embedded Content you see JoyfillComponents listed

Manual Add

Get the latest version of the JoyfillComponents.xcframework and embed it into your application, for example by dragging and dropping the XCFramework bundle onto the Embed Frameworks build phase of your application target in Xcode. Make sure to enable Copy items if needed and Create groups.

Implement your code

🚧 Do not wrap JoyDoc component inside of a UIScrollView. JoyDoc rendering optimizations may not work properly and will introduce unintended bugs.

Example project

We recommend our UIKit/SwiftUI Example's in our repo to help get you started. This will show a readonly or fillable form view depending on the mode you use. Learn more about modes here .

Make sure to replace the userAccessToken inside of Constants.swift file to see your documents you have inside your related Joyfill Manager account.

Code snippet

Make sure to replace the userAccessToken inside of Constants.swift file to see your documents you have inside the. Note that the userAccessToken can be retrieved using the Joyfill Manager and navigating to Settings & Users -> Access Tokens. Below is a simple quick example of it in action (we recommend using our Joyfill Example though as stated above).

Swift:


import UIKit
import JoyfillComponents
import Toast

// Shows the list of documents (not templates, rather submissions)
class JoyDocViewController: UIViewController, onChange, UIImagePickerControllerDelegate & UINavigationControllerDelegate {

    private let vm = JoyDocViewModel()
    var docIdentifier: String = ""

    // MARK: - Components
    private lazy var saveBtn: UIButton = {

        var config = UIButton.Configuration.filled()
        config.title = "Save"
        config.baseBackgroundColor = .systemBlue.withAlphaComponent(0.08)
        config.baseForegroundColor = .systemBlue
        config.buttonSize = .medium
        config.cornerStyle = .medium

        let btn = UIButton(configuration: config)
        btn.translatesAutoresizingMaskIntoConstraints = false
        return btn

    }()

    // MARK: - Lifecycles
    override func viewDidLoad() {
        super.viewDidLoad()

        if let navigationController = navigationController {
            joyfillNavigationController = navigationController
        }
        setup()
        vm.delegate = self
        vm.fetchJoyDoc(identifier: docIdentifier)
    }
}

// MARK: - Composition (Joyfill)
extension JoyDocViewController: JoyDocViewModelDelegate {

    // When the joydoc is fetched from the Joyfill API
    func didFinish() {
        print("Joydoc retrieved did finish.")
        self.title = "Document"

        // MARK: - Setup JoyDoc Form
        // jsonData is the joydocs internal data
        jsonData = vm.activeJoyDoc as! Data
        DispatchQueue.main.async {

            // 1. Setup joydoc form
            let joyfillForm = JoyfillForm()
            joyfillForm.mode = "fill" // or readonly
            joyfillForm.saveDelegate = self
            joyfillForm.translatesAutoresizingMaskIntoConstraints = false

            // 2. Add joydoc to view
            self.view.addSubview(joyfillForm)

            NSLayoutConstraint.activate([
                joyfillForm.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -12),
                joyfillForm.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
                joyfillForm.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                joyfillForm.bottomAnchor.constraint(equalTo: self.saveBtn.topAnchor, constant: 0),

                self.saveBtn.topAnchor.constraint(equalTo: joyfillForm.bottomAnchor, constant: -25),
                self.saveBtn.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 25),
                self.saveBtn.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -25),
                self.saveBtn.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -30)
            ])

            // 3. Handle when user presses an ImageField upload button
            joyfillFormImageUpload = {
                print("Upload images...")

                var alertStyle = UIAlertController.Style.actionSheet
                if (UIDevice.current.userInterfaceIdiom == .pad) {
                    alertStyle = UIAlertController.Style.alert
                }
                let alert = UIAlertController(title: "Choose Image", message: nil, preferredStyle: alertStyle)
                alert.addAction(UIAlertAction(title: "Gallery", style: .default, handler: { _ in
                    self.openImageGallery()
                }))
                alert.addAction(UIAlertAction.init(title: "Cancel", style: .cancel, handler: nil))

                self.present(alert, animated: true, completion: nil)
            }
        }
    }

    func didFail(_ error: Error) {
        print(error)
    }

    // MARK: - Lifecycles -> JoyDoc Handlers
    func handleOnChange(docChangelog: [String : Any], doc: [String : Any]) {
        print("change: ", docChangelog)
    }

    func handleOnFocus(blurAndFocusParams: [String : Any]) {
        print("focus: ", blurAndFocusParams)
    }

    func handleOnBlur(blurAndFocusParams: [String : Any]) {
        print("blur: ", blurAndFocusParams)
    }

    func handleImageUploadAsync(images: [String]) {
        print("images: ", images)
    }

    /* 
        MARK: - Functions to access and fetch image from gallery.
        Keep in mind that you do not have to use image picker you could provide a pre set list of images or don't handle it at all. We are just showing an exampel but once joyfillFormImageUpload is called you can do what you want with that action
     */
    func openImageGallery() {
        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){
            let imagePicker = UIImagePickerController()
            imagePicker.delegate = self
            imagePicker.allowsEditing = true
            imagePicker.sourceType = UIImagePickerController.SourceType.photoLibrary
            self.present(imagePicker, animated: true, completion: nil)
        } else {
            let alert  = UIAlertController(title: "Warning", message: "You don't have permission to access gallery.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            self.present(alert, animated: true, completion: nil)
        }
    }

    public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        self.dismiss(animated: true, completion: nil)
        if let pickedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
            convertImageToDataURI(uri: pickedImage)
        }
    }

    public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        self.dismiss(animated: true, completion: nil)
    }

    // Function to convert UIImage to data URI
    func convertImageToDataURI(uri: UIImage) {
        if let imageData = uri.jpegData(compressionQuality: 1.0) {
            let base64String = imageData.base64EncodedString()
            onUploadAsync(imageUrl: "data:image/jpeg;base64,\(base64String)")
        }
    }

    // MARK: - Handle save button clicked
    @objc func didSave() {        
        // JoyDoc - Form ongoing user field changes
        vm.updateDocumentChangelogs(identifier: docIdentifier, docChangeLogs: docChangeLogs)

        let toast = Toast.text("Form saved ✅")
        toast.show()
        navigationController?.popViewController(animated: true)
    }
}

private extension JoyDocViewController {

    func setup() {

        navigationController?.navigationBar.prefersLargeTitles = false
        view.backgroundColor = .white
        view.overrideUserInterfaceStyle = .light

        view.addSubview(saveBtn)

        saveBtn.addTarget(self,
                          action: #selector(didSave),
                          for: .touchUpInside)

    }

}

ViewModel:


import Foundation
import Alamofire
import SwiftyJSON

struct Constants {
    // MARK: - API

    // Documents endpoint https://docs.joyfill.io/reference/overview-documents
    static let baseURL = "https://api-joy.joyfill.io/v1/documents"

    // See https://docs.joyfill.io/docs/authentication#user-access-tokens
    static let userAccessToken = "<replace_me>"
}

protocol JoyDocViewModelDelegate: AnyObject {
    func didFinish()
    func didFail(_ error: Error)
}

// Retrieves documents (not templates)
class JoyDocViewModel {

    private(set) var activeJoyDoc: Any?

    weak var delegate: JoyDocViewModelDelegate?

    @MainActor
    func fetchJoyDoc(identifier: String) {

        Task { [weak self] in

            let url = "\(Constants.baseURL)/\(identifier)"
            print("Go get documents from \(url)")

            let headers: HTTPHeaders = [
                "Authorization": "Bearer \(Constants.userAccessToken)",
                "Content-Type": "application/json"
            ]

            AF.request(url, method: .get, headers: headers).validate().response { response in
                switch response.result {
                case .success(let value):
                    self?.activeJoyDoc = value
                    self?.delegate?.didFinish()
                    print("Success! Retrieved json (joydoc).")
                case .failure(let error):
                    print(error)
                }
            }

        }
    }

    @MainActor
    func updateDocumentChangelogs(identifier: String, docChangeLogs: Any) {
        do {
            guard let url = URL(string: "\(Constants.baseURL)/\(identifier)/changelogs") else {
                print("Invalid json url")
                return
            }

            let jsonData = try JSONSerialization.data(withJSONObject: docChangeLogs, options: [])

            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.httpBody = jsonData
            request.setValue("Bearer \(Constants.userAccessToken)", forHTTPHeaderField: "Authorization")
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")

            URLSession.shared.dataTask(with: request) { data, response, error in
                if let error = error {
                    print("Error updating changelogs: \(error)")
                } else if let data = data {
                    let json = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
                    let _ = json as? NSDictionary
                }
            }.resume()
        } catch {
            print("Error serializing JSON: \(error)")
        }
    }
}

See parameters for the JoyfillForm SDK instance.

Swift form builder guide summary

This guide serves as a comprehensive resource for developers looking to seamlessly integrate the JoyDoc iOS SDK into Swift projects, with a specific emphasis on UIKit. The inclusion of detailed instructions for both Swift Package Manager and manual installation ensures flexibility in the integration process, catering to diverse developer preferences.

A noteworthy caution is provided, advising against enclosing JoyDoc components within a UIScrollView to avoid potential rendering issues and unintended bugs. The article strongly encourages developers to explore the example projects available in the repository, specifically designed for both SwiftUI and UIKit. These examples showcase readonly and fillable form views, providing practical insights into the usage of Joyfill components.

To facilitate a quick start, a code snippet is shared, underscoring the importance of replacing the userAccessToken in the Constants.swift file. Obtaining this token from the Joyfill Manager, specifically from Settings & Users -> Access Tokens, enables developers to seamlessly access their documents within the Joyfill Manager account.

In essence, this guide not only provides step-by-step instructions for integration but also emphasizes best practices and practical examples, aiming to empower developers with a robust understanding of the JoyDoc iOS SDK for efficient and effective utilization in their Swift projects.