The Brady iOS Print SDK


XCode Prerequisites

Software Requirements

To use the Brady SDK, you must have at least:

  • iOS 15 or higher
  • Swift 5

Add the SDK to your XCode Project

To use the iOS SDK Framework, follow the steps below.

  1. Download the iOS SDK from bradyid.com.
  2. Create a new Xcode Application
  3. After unzipping the "BradySdk-iOS.zip", drag and drop the ".xcframework" file to the root of your app in the file tree.

  4. Xcode will now show the framework under the "Frameworks, Libraries, and Embedded Content" section in the App Target.

  5. Change the Embedded field to "Embed & Sign."

  6. Test that everything is added correctly by typing "import BradySdk" at the top of any file in your app (it will autofill the name if it can find the framework).

Adding Permissions

In order to use Bluetooth and Wi-Fi discovery and connection within the SDK, you must let the user allow these permissions the first time the application is opened. This pop-up will appear automatically when these permissions are added to the "info.plist" in your app.

  • Under the "Info" tab off your app settings, add the following permissions underneath "Custom Target Properties."

    • Privacy - Bluetooth Always Usage Description
    • Privacy - Bluetooth Peripheral Usage Description
    • Privacy - Local Network Usage Description
    • Bonjour services:
      • Enter the value "_pdl-datastream._tcp." for the Key "Item 0".

  • You may enter whatever message you would like under the value column for these permissions (excluding Bonjour Services).
  • To verify this worked, run your application and you will see a "pop up" window asking you to allow the permissions.
  • If a user cancels the pop up and selects "Don't Allow", they can always allow these permissions in their device's "Settings" application.

Versions >=1.7.0

Custom Brady fonts are no longer embedded in the iOS SDK. Therefore, they must be downloaded here.

The custom Brady fonts only need to be downloaded and embedded into your application if the BWT files being used by your application were designed with any of these fonts. To embed them in your iOS application:

  • Create a folder named "fonts" in your application's "Resource" directory.
  • Add the desired fonts to this new folder.
  • Drag and drop each font to the "Copy Bundle Resources" section to add the fonts to the main bundle of your application.
    • In XCode click your application's .xcodeproj file >> Build Phases Tab >> Expand the Copy Bundle Resources dropdown >> this is where you drag and drop the fonts.

Printer Discovery

Before starting this tutorial, it is important to note that the SDK was not designed to discover, connect, print, and disconnect all at once. The printer itself will still struggle to receive and send data this quickly.

Example: It is not recommended to design an app with a single button labeled "Print" where the app would discover, connect, print, and then disconnect on the button click.


Requesting Permissions

In order to discover nearby printers, the user must approve the required permissions:

(Reference the Setup page for the iOS SDK)

  • This should prompt the user only on the initial opening of the application.
    • If the user does not allow permissions when prompted, they can always go into the settings app to allow them later. However, the user will never be able to discover printers without approving these permissions.

Implementing Discovery

  • Create a new PrinterDiscovery object by initializing a listeners array and passing it to the factory:
var listeners = [PrinterDiscoveryListener]()
listeners.append(self)  // Implement PrinterDiscoveryListener in your class
var printerDiscovery: PrinterDiscovery = PrinterDiscoveryFactory.getPrinterDiscovery(listeners: listeners)
  • Implement the PrinterDiscoveryListener protocol in your model class:
    • This requires you to implement the printerDiscovered and printerRemoved methods:
public func printerDiscovered(discoveredPrinterInformation: DiscoveredPrinterInformation) {
    if(!foundPrinters.contains(where: {discoveredPrinterInformation.getName() == $0.getName() && discoveredPrinterInformation.getConnectionType().rawValue == $0.getConnectionType().rawValue})) {
        foundPrinters.append(discoveredPrinterInformation)
    }
}

public func printerRemoved(discoveredPrinterInformation: DiscoveredPrinterInformation) {
    DispatchQueue.main.async {
        foundPrinters.removeAll(where: {
            $0.getName() == discoveredPrinterInformation.getName() && $0.getConnectionType() == discoveredPrinterInformation.getConnectionType()
        })
    }
}
  • Start discovering nearby printers by calling both BLE and Wi-Fi discovery methods:
printerDiscovery.startBlePrinterDiscovery() //or 'startBluetoothPrinterDiscovery()' instead if your printer is a Bluetooth Classic device
printerDiscovery.startWifiPrinterDiscovery()
  • To stop the discovery scan, call:
printerDiscovery.stopPrinterDiscovery()
  • To find the most recent printer you successfully connected to, call:
let lastConnectedPrinter = printerDiscovery.getLastConnectedPrinterName()

Displaying Printers

Swift's @Published and @ObservedObject property wrappers enable automatic UI updates when data changes.

In your model class, mark the printers list as published:

@Published private var foundPrinters = [DiscoveredPrinterInformation]()

Your model class should conform to both PrinterUpdateListener and PrinterDiscoveryListener, and inherit from ObservableObject:

public class Model: PrinterUpdateListener, PrinterDiscoveryListener, ObservableObject {
    @Published private var foundPrinters = [DiscoveredPrinterInformation]()
    // ... other properties
}

In your SwiftUI view, observe the model:

struct ContentView: View {
    @ObservedObject var model: Model
    // ... rest of view
}

Now you can display the printers in a list, and the UI will automatically update whenever printers are discovered or removed:

List(model.getFoundPrinterNames(), id: \.self) { printerName in
    Text(printerName)
}

Printer Connection


Connecting to a Printer

The connectToDiscoveredPrinter method uses the initialized PrinterDiscovery object to connect to a printer and returns a PrinterDetails object.

var listeners: Array<PrinterUpdateListener> = []
listeners.append(self)

// Iterate through all found printers
var printerToConnectTo: DiscoveredPrinterInformation? = nil
for printer in foundPrinters {
    if printer.getName() == printerSelected {
        printerToConnectTo = printer
        printerDiscovery.stopPrinterDiscovery()
    }
}

// Connect to the found printer
do {
    if printerDetails == nil {
        printerDetails = try await printerDiscovery.connectToDiscoveredPrinter(printerSelected: printerToConnectTo, listeners: listeners)
    }
} catch let error {
    debugPrint(error)
    return false
}

Key points:

  • The printerSelected parameter is the DiscoveredPrinterInformation object representing the printer to connect to.
  • The listeners parameter is an array of PrinterUpdateListener objects. The model class should implement this protocol to receive printer status updates.
  • After successful connection, you'll receive a PrinterDetails object with printer information (name, model, battery level, supply information, etc.).
  • If connection fails, it will return nil.
  • The printerDetails.haveOwnership() method can be used to check if the device owns the printer (relevant for M211 models).

Automatic Connection

Once your application has connected to a printer for the first time, it will store the printer object internally. In the future, you can add a feature to your app that grabs this object and connects to it automatically without the user manually selecting a printer from the UI:

//Use this PrinterDiscovery method to find the name of the printer available to automatically connect to.
lastConnectedPrinterName = printerDiscovery.getLastConnectedPrinterName()
//Now, whenever a printer is discovered, it'll compare it immediately to this printer name.
//If it matches, it'll attempt to connect right away when the app starts.
if newlyDiscoveredPrinter == model.lastConnectedPrinterName {
    connected = try await model.connectToDiscoveredPrinter(printerSelected: printerToConnectTo!, listeners: listeners)
}

Calling printerDetails.disconnect() will make the app "forget" the current printer internally and will not connect to it automatically afterwards. Alternatively, calling printerDetails.disconnectWithoutForget() will disconnect from the printer temporarily, but getLastConnectedPrinterName() will still return the name of the printer you just disconnected from.


OWNERSHIP NOTE: Using an M211 introduces the concept of "ownership". When connecting to an M211 with a mobile device for the first time, the blue light on the M211 should be blinking. If the light is solid blue, another mobile device "owns" the printer and only that device will be able to connect. In the event that a connection fails, a device can still have "ownership". Therefore, being connected and having ownership are not synonymous. To connect using a different mobile device, hold the power button for five seconds to release ownership.


Disconnecting from Printers

public func disconnect() async {
    do {
        let detailsToDisconnect = printerDetails
        printerDiscovery.forgetLastConnectedPrinter()

        let result = try await detailsToDisconnect?.disconnect() ?? false

        await MainActor.run {
            self.isDisconnected = true
            self.lastConnectedPrinterName = nil
            self.printerDetails = nil
        }

        if result {
            print("Disconnect Successful!")
        } else {
            print("Disconnect Failed!")
        }
    }
    catch let error {
        debugPrint(error)
    }
}

After disconnecting, you can call printerDiscoveryStarted() to begin discovering printers again.


Performing Printer Operations


Basic Operations (M211 & M511)

Cut a label:

public func cutLabel() async {
    do {
        let result = try await printerDetails?.cutSupply() ?? false
        if result {
            print("Operation Successful!")
        } else {
            print("Operation Failed!")
        }
    }
    catch let error {
        debugPrint(error)
    }
}

Feed a label:

public func feed() async {
    do {
        let result = try await printerDetails?.feedSupply() ?? false
        if result {
            print("Operation Successful!")
        } else {
            print("Operation Failed!")
        }
    }
    catch let error {
        debugPrint(error)
    }
}

Set automatic shutdown timer:

public func setAutomaticShutdown(minutes: Int) async {
    do {
        let result = try await printerDetails?.setAutomaticShutdownTime(timeInMinutes: minutes) ?? false
        if result {
            print("Automatic Shutdown Timer Set Successfully!")
        } else {
            print("Failed to Set Automatic Shutdown Timer!")
        }
    }
    catch let error {
        debugPrint(error)
    }
}

Inkjet Printer Operations

Clean Printhead:

public func cleanPrinthead() async {
    do {
        let result = try await printerDetails?.cleanPrinthead() ?? false
        if result {
            print("Clean Printhead Successful!")
        } else {
            print("Clean Printhead Failed!")
        }
    } catch {
        debugPrint(error)
    }
}

Reset Maintenance Station:

public func setMaintenanceStationReset() async {
    do {
        let result = try await printerDetails?.setMaintenanceStationProperty(station: .Reset) ?? false
        if result {
            print("Maintenance Reset Successful!")
        }
    } catch {
        debugPrint(error)
    }
}

Print Alignment Label:

public func printAlignmentLabel() async {
    do {
        let result = try await printerDetails?.printAlignmentLabel() ?? false
        if result {
            print("Print Alignment Label Successful!")
        }
    } catch {
        debugPrint(error)
    }
}

Set Alignment Offset:

public func setAlignmentOffset(offset: Int) async {
    do {
        let result = try await printerDetails?.setAlignmentOffset(offsetValue: offset) ?? false
        if result {
            print("Set Alignment Offset Successful!")
        }
    } catch {
        debugPrint(error)
    }
}

Set Quality Mode:

public func setPrinterQualityMode(mode: PrintMode) {
    do {
        try printerDetails?.setPrinterQualityMode(qualityMode: mode)
        print("Set Quality Mode to \(mode)")
    } catch {
        debugPrint(error)
    }
}

Monitoring Printer Status Changes

Implement the PrinterUpdateListener protocol to receive real-time printer status updates:

public func PrinterUpdate(changedProperties: Array<PrinterProperties>) {
    DispatchQueue.main.async {
        for property in changedProperties {
            switch property {
            case PrinterProperties.CurrentStatus:
                let updateMessage = self.printerDetails?.getPrinterStatusMessage()
                switch updateMessage {
                case "PrinterStatus_Disconnected":
                    self.toastMessage = "Disconnected"
                    self.isDisconnected = true
                case "PrinterStatus_Connecting":
                    self.toastMessage = "Connected"
                case "PrinterStatus_Initialized":
                    self.toastMessage = "Connected"
                case "PrinterStatus_HeadOpen_ErrorBody":
                    self.toastMessage = "Head Open Error"
                default:
                    self.toastMessage = updateMessage ?? "Ready"
                }
            case PrinterProperties.SupplyName:
                self.supplyName = self.printerDetails?.getSupplyName() ?? ""
            case PrinterProperties.SupplyWidth, PrinterProperties.SupplyHeight:
                self.supplyDimensions = self.printerDetails?.getSupplyDimensions() ?? ""
            case PrinterProperties.SupplyRemainingPercent:
                self.supplyRemaining = String(self.printerDetails?.getSupplyRemainingPercentage() ?? 0)
            case PrinterProperties.BatteryChargePercentage:
                self.batteryLevel = String(self.printerDetails?.getBatteryLevelPercentage() ?? 0) + "%"
            default:
                break
            }
        }
    }
}

Always dispatch UI updates on the main thread using DispatchQueue.main.async to avoid data races.

Connect/Disconnect Scenarios

Scenario #1 — Unexpected Disconnect:

If a connected printer unexpectedly disconnects (power off, out of range), the SDK sends a PrinterStatus_Disconnected update. Listen for this in your PrinterUpdate() implementation and update the UI accordingly:

case PrinterProperties.CurrentStatus:
    let updateMessage = self.printerDetails?.getPrinterStatusMessage()
    if updateMessage == "PrinterStatus_Disconnected" {
        self.toastMessage = "Disconnected"
        self.isDisconnected = true
        // Update UI to disable print button, show reconnection options, etc.
    }

Scenario #2 — First-Time Connection:

When connecting for the first time, check if printerDetails is nil before attempting connection:

if printerDetails == nil {
    printerDetails = try await printerDiscovery.connectToDiscoveredPrinter(printerSelected: printerToConnectTo, listeners: listeners)
}

If the app closes and reopens, printerDetails will be nil again, requiring a fresh connection via discovery.

Scenario #3 — M211 Ownership:

When connecting to an M211, the device must "own" the printer to communicate with it. A solid blue light indicates another device owns the printer; a blinking blue light means the printer is ready to be owned. To release ownership, hold the power button for 5 seconds. Check ownership after connection with:

let hasOwnership = printerDiscovery.getHaveOwnership()

Open Templates

Before adding code, create a subdirectory within your app's root directory to place template files (.BWT).

Initialize a Template Object

Create a dropdown menu in SwiftUI to allow users to select a template:

Menu {
    Picker("Template Picker", selection: $selection) {
        if model.printerDetails != nil {
            ForEach(model.getTemplatesForPrinter(), id: \.self) {
                Text($0).font(.footnote)
            }
        }
    }.onChange(of: selection) {
        // Update preview when selection changes
        self.image = templatePreview(selection: selection)
    }
} label: {Text(selection).font(.caption).padding()}

In your model, implement the getSelectedTemplate() method to load template files:

public func getSelectedTemplate(context: AppContext, selection: String) -> Template? {
    do {
        self.selection = selection
        let filePath = URL(fileReferenceLiteralResourceName: selection)
        let templateData: Data = try Data(contentsOf: filePath)

        // Encode to base64 for proper handling
        let base64: String = templateData.base64EncodedString()
        guard let base64Data = base64.data(using: .utf8) else {
            print("Could not get the desired Template object.")
            return nil
        }

        guard let templateData2 = Data(base64Encoded: base64Data) else {
            print("Could not get the desired Template object.")
            return nil
        }

        let iStream: InputStream = InputStream(data: templateData2)
        return TemplateFactory.getTemplate(template: iStream)
    }
    catch let error {
        print(error)
    }
    return nil
}

Note: Template files (.BWT) must be in a subdirectory at your project's root for fileReferenceLiteralResourceName to find them.

Setting Placeholder Values

After loading a template, use the getTemplateData() method to access and modify placeholder values. The approach depends on the template type:

For Text Templates:

public func setPrintPreview(template: Template?, firstValue: String, secondValue: String?) -> UIImage? {
    if template != nil {
        do {
            if selection.contains("Text") {
                for entity in try template!.getTemplateData() {
                    if entity.getName() == "TEXT 1" {
                        try entity.setValue(value: firstValue)
                    }
                    else if entity.getName() == "TEXT 2" {
                        try entity.setValue(value: secondValue ?? "")
                    }
                }
            }
        } catch {
            // Handle error
        }
    }
    return nil
}

For Barcode Templates:

if selection.contains("Barcode") {
    for entity in try template!.getTemplateData() {
        if entity.getName() == "BARCODE 1" && firstValue.count > 2 {
            try entity.setValue(value: firstValue)
        }
    }
}

To determine the type of a placeholder, use getTemplateObjectType(), which returns values like StaticText, Text, Rectangle, Barcode, Image, PolyPolyLine, or Other.

Preview the Template

Call template.getPreview() to generate a preview image of the template. Pass the printerDetails parameter to ensure the preview accounts for printer-specific properties like ribbon and supply colors:

func setPrintPreview(template: Template?, firstValue: String, secondValue: String?) -> UIImage? {
    if template != nil {
        do {
            // Set placeholder values (see Setting Placeholder Values section)
            let cgImage = try template?.getPreview(
                labelNumber: 0, 
                dpi: 96, 
                maxPixelWidthAndHeight: 200, 
                printerDetails: self.printerDetails
            )
            self.template = template!
            if cgImage == nil {
                self.printPreview = UIImage()
            }
            else {
                self.printPreview = UIImage(cgImage: cgImage!)
            }
        }
        catch {
            // Handle error
        }
        return self.printPreview
    }
    return nil
}

Use this in SwiftUI to display the preview:

Image(uiImage: model.getPrintPreview())
    .resizable()
    .scaledToFit()
    .frame(maxWidth: UIScreen.main.bounds.size.width * 0.8, maxHeight: UIScreen.main.bounds.size.height * 0.25)

Printing

Printing Templates

To print a template, ensure the printer is connected and the template has been loaded with placeholder values set:

public func printTemplate() async {
    // Guard: must be connected before printing.
    guard let printerDetails = printerDetails else {
        await MainActor.run { self.toastMessage = "Not connected to a printer." }
        return
    }
    // Guard: template must be loaded via setPrintPreview() before printing.
    guard let template = template else {
        await MainActor.run { self.toastMessage = "No template selected." }
        return
    }

    let options = PrintingOptions()
    options.cutOption = CutOption.EndOfJob
    options.numberOfCopies = 1
    options.isCollated = true
    let dontPrintTrailerFlag = selection.contains("Cable-Wrap")

    do {
        let status = try await printerDetails.print(
            template: template,
            printingOptions: options,
            dontPrintTrailerFlag: dontPrintTrailerFlag
        )
        debugPrint("Printing Result: \(status)")
    } catch {
        debugPrint(error)
        await MainActor.run { self.toastMessage = "Printing failed: \(error.localizedDescription)" }
    }
}

Printing Images

To print an image (PNG, JPG, JPEG, WEBP, or SVG) or PDF:

public func printImage(selection: String) async {
    guard let printerDetails = printerDetails else {
        await MainActor.run { self.toastMessage = "Not connected to a printer." }
        return
    }

    let printingOptions = PrintingOptions()
    printingOptions.cutOption = CutOption.EndOfJob
    printingOptions.numberOfCopies = 1
    printingOptions.isCollated = true

    do {
        let components = selection.split(separator: ".")
        guard let name = components.first else {
            print("Error: Invalid file name format.")
            return
        }

        let ext = components.count > 1 ? String(components.last!) : "pdf"
        if ext == "pdf" {
            guard let pdfURL = Bundle.main.url(forResource: String(name), withExtension: ext) else {
                print("Error: Could not find PDF named: \(selection)")
                return
            }
            let data = try Data(contentsOf: pdfURL)
            let status = try await printerDetails.printPDF(
                fileData: data, 
                rotationDegrees: 0,
                printingOptions: printingOptions, 
                dontPrintTrailerFlag: false
            )
            debugPrint("Printing Result: \(status)")
        } else {
            guard let image = UIImage(named: selection) else {
                print("Error: Could not load image.")
                return
            }
            let status = try await printerDetails.print(
                images: [image],
                printingOptions: printingOptions,
                dontPrintTrailerFlag: false
            )
            debugPrint("Printing Result: \(status)")
        }
    } catch {
        debugPrint(error)
        await MainActor.run { self.toastMessage = "Printing failed: \(error.localizedDescription)" }
    }
}

Printing with RFID Encoding (i7500)

For RFID-enabled printers like the i7500, you can encode data onto RFID tags during printing:

public func printTemplateWithRfid() async {
    guard let printerDetails = printerDetails else {
        await MainActor.run { self.toastMessage = "Not connected to a printer." }
        return
    }
    guard let template = template else {
        await MainActor.run { self.toastMessage = "No template selected." }
        return
    }

    let options = PrintingOptions()
    options.cutOption = CutOption.EndOfJob
    options.numberOfCopies = 3  // Print 3 labels with different RFID data
    options.isCollated = true

    // Define RFID operations for each label
    let rfidOperations: [[RfidOperation]] = [
        // Label 1 RFID data
        [
            try RfidOperation(
                command: RfidCommandType.Write,
                inputType: RfidInputType.ASCII,
                location: RfidLocation.ElectronicProductCode,
                data: "1AAAAAAAAAA",
                password: nil,
                offset: 0,
                numberOfBlocks: nil
            ),
            try RfidOperation(
                command: RfidCommandType.Write,
                inputType: RfidInputType.HEX,
                location: RfidLocation.User,
                data: "AAAAAAAAAA",
                password: nil,
                offset: 0,
                numberOfBlocks: nil
            )
        ],
        // Label 2 RFID data
        [
            try RfidOperation(
                command: RfidCommandType.Write,
                inputType: RfidInputType.ASCII,
                location: RfidLocation.ElectronicProductCode,
                data: "2BBBBBBBBBB",
                password: nil,
                offset: 0,
                numberOfBlocks: nil
            ),
            try RfidOperation(
                command: RfidCommandType.Write,
                inputType: RfidInputType.HEX,
                location: RfidLocation.User,
                data: "BBBBBBBBBB",
                password: nil,
                offset: 0,
                numberOfBlocks: nil
            )
        ],
        // Label 3 RFID data
        [
            try RfidOperation(
                command: RfidCommandType.Write,
                inputType: RfidInputType.ASCII,
                location: RfidLocation.ElectronicProductCode,
                data: "3CCCCCCCCCC",
                password: nil,
                offset: 0,
                numberOfBlocks: nil
            ),
            try RfidOperation(
                command: RfidCommandType.Write,
                inputType: RfidInputType.HEX,
                location: RfidLocation.User,
                data: "CCCCCCCCCC",
                password: nil,
                offset: 0,
                numberOfBlocks: nil
            )
        ]
    ]

    do {
        let status = try await printerDetails.print(
            template: template,
            rfidOperations: rfidOperations,
            printingOptions: options,
            dontPrintTrailerFlag: false
        )
        debugPrint("RFID Printing Result: \(status)")
    } catch {
        debugPrint(error)
        await MainActor.run { self.toastMessage = "RFID printing failed: \(error.localizedDescription)" }
    }
}

RFID Parameters:

  • rfidOperations: A 2D array where each inner array represents operations for one label. Each RfidOperation specifies:
  • command: RfidCommandType.Write (or other supported commands)
  • inputType: RfidInputType.ASCII or RfidInputType.HEX
  • location: RfidLocation.ElectronicProductCode or RfidLocation.User (memory location on the tag)
  • data: The data to encode (ASCII or HEX string)
  • password: Optional password for tag security (nil if not needed)
  • offset: Starting offset in memory (typically 0)
  • numberOfBlocks: Number of memory blocks (nil for default)