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.
This will open a new window with different extensions. Let's search for the "Widget Extension".
Next, we can select the product name of our widget.
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".
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.
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.
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])
- 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)
}
}
- 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)
}
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.
Then search for "App Groups" and double click on it.
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.
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])
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:
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)
}
}
}
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)
}
}
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.
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
}
}
When we run the application again, the widget should be displaying both the text and the image, as shown below:
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.
Next, let's name the file "WidgetModule" and select your app as the target of this file.
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
}
}
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":
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
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".
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"
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])
})
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!
0 comments