Swift

How to getting started with Joyfill in a Swift UIKit/SwiftUI project or even an Objective-C project.

Overview

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 your the file per Apple Developer Guides suggest (we have included an example as well).

Guide

Setup

Please see our Overview and Getting Started Guide

Project 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 https://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).


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)
        
    }
    
}


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)")
        }
    }
}

JoyDoc -> Form()

Below are parameters for the JoyfillForm SDK instance. Each shows

NameTypeDescription
mode'fill' | 'readonly'Enables and disables certain JoyDoc functionality and features.
• fill is the mode where you simply input the field data into the form
• readonly is the mode where everything in the form is set to read-only.
docObjectThe default JoyDoc JSON starting object to load into the component view. Must be in the JoyDoc JSON data structure.
handleOnChangeFuncUsed for responding to change logs and any realtime change a user is making to a form.
handleOnFocusFuncUsed for realtime detection of any field that gets focused.
handleOnBlurFuncUsed for realtime detection of any field that gets blurred (loses focus).
joyfillFormImageUploadFuncUsed for handling any user uploaded images to the JoyDoc ImageFields. This is used for handling when a user taps to upload images to the field. Upon successful upload you will use the handleOnChange -> docChangelog param to receive the uploaded images.
Images
uploadImageTapActionFuncFor handling the uploading of images