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
Installation
Swift Package Manager
- To integrate using Swift Package Manager, Swift version >= 5.3 is required.
- 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:
- 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.
- Select components-ios package
- Choose your Dependency Rule (we recommend Up to Next Major Version).
- Select the project to which you would like to add JoyDoc, then click Add Package
- 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.