How to build a Barcode Widget in React Native (part II: iOS)

jelpet

Jelena Petkovic

Posted on February 14, 2023

How to build a Barcode Widget in React Native (part II: iOS)

In this tutorial we are going to learn how to build a barcode widget for an iOS device.
In the previous part we focused on Android and Java, while in this part we're going to be using Swift.
The process will be similar in many ways, except for the bridging method, which is going to slightly differ.

In part I of this series we will show how to create Android widget using Java.

1. Intro

In order to build the widget we will be creating a simple extension in xCode as well as a "bridge" that will allow our app to communicate with the widget. Let's begin!

2. Creating the widget

We can start by opening our project in xCode. Now, right click on the project folder and navigate to file > new > target.

Image description

This will open a new window with different extensions. Let's search for the "Widget Extension".

Image description

Next, we can select the product name of our widget.

Image description

I am going to name it "BarcodeWidget". Make sure to uncheck “Include Configuration Intent” and "Include live activity".

If we click the finish button, xCode is going to ask us if we want to create an active scheme. Select "activate".

Image description

Now, we can select our widget extension in the "targets" section and run it. Be sure to run it for iOS 14.0 and up, as the previous versions of iOS aren't compatible with the extension.

At this point, we should be able to see our widget on the home screen. For now, it is only displaying a simple date.

Image description

Let's take a look at the files created in our project. We can see that xCode created a new folder with the same name we set for the widget at the beginning.

Image description

To modify our widget we can open the "BarcodeWidget.swift" file.
There are different functions and structures inside the file. Let's take a look at a few of them:

  • "BarcodeWidget struct" deals with the main configuration of our widget. Here, we can set the display name as well as the widget's description. If we'd like our widget to be non-resizable and of a specific size (the default preview offers three options: small, medium and large) we can set it here by calling the supportedFamilies() function and passing it an argument, an array that contains our preferred widget sizes.

.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])

Image description

  • Next, there is the "BarcodeWidgetEntryView" struct where we can modify the widget's layout.
struct BarcodeWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}
Enter fullscreen mode Exit fullscreen mode
  • You may also find the "getTimeline() method". It provides an array of timeline entries for the current time and, optionally, any future times to update a widget.
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
Enter fullscreen mode Exit fullscreen mode

If you want a better understanding of the TimelineProvider and its functions, you can always check Apple's official documentation in here.

3. Bridging

To display data from our React Native app, we are going to need some additional settings.
First, let's add the "App Group" capability to our project in order to let the widget communicate with the app.
Find your main app target in the Xcode project settings, switch to the Signing & Capabilities tab and click on + Capability.

Image description

Then search for "App Groups" and double click on it.

Image description

Click the + button in the new "App Groups" capability created and choose your team. The final step is to add an app group identifier. After you finish, you should see your App Group id appear. If it comes up red - make sure to try the refresh button.

Image description

Now, we can do the same thing for our widget's target. We can open the widget's Signing & Capabilities tab and add a new "App Groups" capability. If we select the same team as before, we should be able to see the identifier we already created in our main project's target. Make sure the same id is selected on both targets.

We have to set one more thing before we start writing our code in React Native. This includes installing the "SharedGroupPreferences" package. From the terminal, run:

npm i react-native-shared-group-preferences

Open your App file and import:

import SharedGroupPreferences from 'react-native-shared-group-preferences'

Now, let's add this to our code:

  // Let's display a random 6 digit number
  const barcode = Math.floor(100000 + Math.random() * 900000).toString()

  const appGroupIdentifier = 'group.widget.barcode.jp'

  useEffect(() => {
  if (Platform.OS === 'ios'){
      const setWidgetData = async () => {await SharedGroupPreferences.setItem('widgetKey', {
        text: barcode !== undefined ? barcode : '',
      }, appGroupIdentifier)}
      setWidgetData()
        .catch((error) => {
          log.info(() => ['error setting widget data, err: ', error])
  },[barcode])
Enter fullscreen mode Exit fullscreen mode

The "appGroupIdentifier" should be the same as the one we just set in our "App Groups".

Great! We are done with the React Native part. Now, let's go back to our widget's code.

To add new text to our widget, first we have to add the following code above our Provider:

struct WidgetData: Decodable {
var text: String
}

Next, update the "SimpleEntry" struct:

struct SimpleEntry: TimelineEntry {
let date: Date
let myString: String
}

Variable "myString" is going to be our barcode string that we set in the React Native app. When we add this, an error is going to come up - telling you to update the number of arguments of certain methods. After fixing this error, our code should look something like this:

Image description

Now let's update our "getTimeline()" method with the following code:

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []

    let userDefaults = UserDefaults.init(suiteName: "group.widget.barcode.jp")
    if userDefaults != nil {
      if let savedData = userDefaults!.value(forKey: "widgetKey") as? String {
        let decoder = JSONDecoder()
        let data = savedData.data(using: .utf8)
        if let parsedData = try? decoder.decode(WidgetData.self, from: data!) {
          let currentDate = Date()
          let entryDate = Calendar.current.date(byAdding: .second, value: 3, to: currentDate)!

          let entry = SimpleEntry(date: entryDate, myString: parsedData.text)
          entries.append(entry)

          let timeline = Timeline(entries: entries, policy: .atEnd)
          completion(timeline)
        } else {
          print("Could not parse data")
        }
      } else {
        let currentDate = Date()

        for hourOffset in 0 ..< 2 {
          let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset, to: currentDate)!
          let entry = SimpleEntry(date: entryDate, myString: "No data")
          entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Replace the "suiteName" value with your own App Group ID in the following line:

UserDefaults.init(suiteName: "group.widget.barcode.jp")

The last thing to set before running our widget again is the "BarcodeWidgetEntryView" struct.
In order to display the barcode text from the React Native app - let's add this:

`struct BarcodeWidgetEntryView : View {
var entry: Provider.Entry

var body: some View {
   Text(entry.myString).font(.system(size: 12)).tracking(2)
}
Enter fullscreen mode Exit fullscreen mode

}`

Remember to run pod install in your iOS folder.
Now, let's start the app.

If everything works well, the barcode number we previously set in our app will be displayed on the widget.

Image description

4. Generating the barcode image from a string value

The only thing that's left to do includes generating a barcode image from our barcode number as well as displaying it.

To do this, we are going to add a few functions to our "BarcodeWidget.swift" file.

The first function we are going to add is "generateBarcode()":

func generateBarcode(from string: String) -> UIImage? {

  let data = string.data(using: String.Encoding.ascii)

  if let filter = CIFilter(name: "CICode128BarcodeGenerator") {
    filter.setDefaults()
    //Margin
    filter.setValue(1.00, forKey: "inputQuietSpace")
    filter.setValue(data, forKey: "inputMessage")
    //Scaling
    let transform = CGAffineTransform(scaleX: 3, y: 3)

    if let output = filter.outputImage?.transformed(by: transform) {
        let context:CIContext = CIContext.init(options: nil)
        let cgImage:CGImage = context.createCGImage(output, from: output.extent)!
        let rawImage:UIImage = UIImage.init(cgImage: cgImage)

        //Refinement code to allow conversion to NSData or share UIImage. Code here:
        //http://stackoverflow.com/questions/2240395/uiimage-created-from-cgimageref-fails-with-uiimagepngrepresentation
        let cgimage: CGImage = (rawImage.cgImage)!
        let cropZone = CGRect(x: 0, y: 0, width: Int(rawImage.size.width), height: Int(rawImage.size.height))
        let cWidth: size_t  = size_t(cropZone.size.width)
        let cHeight: size_t  = size_t(cropZone.size.height)
        let bitsPerComponent: size_t = cgimage.bitsPerComponent
        //THE OPERATIONS ORDER COULD BE FLIPPED, ALTHOUGH, IT DOESN'T AFFECT THE RESULT
        let bytesPerRow = (cgimage.bytesPerRow) / (cgimage.width  * cWidth)

        let context2: CGContext = CGContext(data: nil, width: cWidth, height: cHeight, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: cgimage.bitmapInfo.rawValue)!

        context2.draw(cgimage, in: cropZone)

        let result: CGImage  = context2.makeImage()!
        let finalImage = UIImage(cgImage: result)

        return finalImage

    }
}

Enter fullscreen mode Exit fullscreen mode

This function will create an UIImage from the string.

Finally, let's update "BarcodeWidgetEntryView" by adding the following:

struct BarcodeWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
      if let image = generateBarcode(from: entry.myString) {
                          Image(uiImage: image).resizable()
                          .imageScale(.small).frame( width: CGFloat(132), height: CGFloat(60)).padding(.bottom, 3)
       }
       Text(entry.myString).font(.system(size: 12)).tracking(2)
    }
}
Enter fullscreen mode Exit fullscreen mode

When we run the application again, the widget should be displaying both the text and the image, as shown below:

Image description

5. Updating the widget content

Now, let's focus on keeping our widget up to date.
First let's change our update policy to "never" since we are going to instruct the app on when to update its content.

let timeline = Timeline(entries: entries, policy: .never)

Let's create a "reloadWidget()" function in the Swift code that we are later going to call in our React Native app.
This can be done by adding two files to our project.
Select "new file" and choose "Swift File" in the next window.

Image description

Next, let's name the file "WidgetModule" and select your app as the target of this file.

Image description

Update the "WidgetModule.swift" file as shown below:

import Foundation
import AVFoundation
import WidgetKit

@objc(WidgetModule)
class WidgetModule: NSObject {

  @objc public func reloadWidget(_ kind: String) -> Void {
     if #available(iOS 14.0, *) {
       #if arch(arm64) || arch(i386) || arch(x86_64)
       WidgetCenter.shared.reloadAllTimelines()
       #endif
     }
   }

  @objc
  static func requiresMainQueueSetup() -> Bool {
    return true
  }

}
Enter fullscreen mode Exit fullscreen mode

Our "reloadWidget" is using WidgetCenter.shared.reloadAllTimelines()
method to reload the widget's timeline. For more details about reloading the widget check this link.

Now we need to add the other file. Choose "Objective-C":

Image description

Let's name it the same as the file before "WidgetModule" and select the same file target as of the previously created file.

Update the file like this:

#import <Foundation/Foundation.h>

#import "React/RCTBridgeModule.h"
@interface
RCT_EXTERN_MODULE(WidgetModule, NSObject)
RCT_EXTERN_METHOD(reloadWidget: (NSString *)kind)

@end
Enter fullscreen mode Exit fullscreen mode

This file will be used to export our newly created module that we are going to use later in the React Native app.

The last file that we are going to add is a Bridging Header. Again, add a new file and choose "Header".

Image description

Paste the following code:

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "React/RCTBridgeModule.h"
Enter fullscreen mode Exit fullscreen mode

Finally let's call our "updateWidget" function from the React Native code;

First, import NativeModules in the App file:

`import { NativeModules } from 'react-native'

export const App = () => {
const { WidgetModule } = NativeModules

...
}
`

Then, let's update our "useEffect" hook by adding
.then(() => WidgetModule.reloadWidget(appGroupIdentifier))
to setWidgetData() method.

The final result should look like this:


  if (Platform.OS === 'ios'){
      const setWidgetData = async () => {await SharedGroupPreferences.setItem('widgetKey', {
        text: barcode !== undefined ? barcode : '',
      }, appGroupIdentifier)}
      setWidgetData()
        .then(() => WidgetModule.reloadWidget(appGroupIdentifier))
        .catch((error) => {
          log.info(() => ['error setting widget data, err: ', error])
        })
Enter fullscreen mode Exit fullscreen mode

7. The end

Alright, it looks like we are done!

Don't forget to run npm install && cd iOS && pod install before running your project again. The widget layout should now be updating every time we call the "updateWidget" function.

Feel free to play around with different widget options and try to add some style to it.

Good luck with your project, I hope this tutorial helped you!

💖 💪 🙅 🚩
jelpet
Jelena Petkovic

Posted on February 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related