I’m creating widget app in react native cli. It’s a countdown widget that has 5 backgrounds which the consumer can decide. And the consumer also can add customized picture to the background of the widget. When i attempted to make use of NativeModules.RNWidgetModule, it’s being null. How am i able to remedy this?
(I’m not IOS developer. I wrote ios codes with AI.)
SkinPicker.ts
/* eslint-disable react-native/no-inline-styles */
import React from 'react';
import {
View,
TouchableOpacity,
StyleSheet,
Textual content,
BackHandler,
Platform,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { NativeModules } from 'react-native';
const { WidgetModule } = NativeModules;
// Helper to use pores and skin
const setWidgetSkin = (skinName: string) => {
if (Platform.OS === 'android' && WidgetModule) {
WidgetModule.setSkin(skinName);
}
if (Platform.OS === 'ios' && NativeModules.RNWidgetModule) {
NativeModules.RNWidgetModule.setSkin(skinName);
NativeModules.RNWidgetModule.reloadWidgets();
}
if (Platform.OS === 'android') {
BackHandler.exitApp();
}
};
// Pores and skin definitions
const skins = [
{
name: 'FIDESZ',
type: 'solid',
color: '#FF6A13',
},
{
name: 'TISZA',
type: 'gradient',
colors: ['#24B573', '#ED4551'],
},
{
title: 'KUTYAPART',
sort: 'dots', // two-color cut up + two purple dots
colours: ['#FFFFFF', '#000000'],
dotColor: '#DA0000',
},
{
title: 'DK',
sort: 'gradient',
colours: ['#0062A7', '#C50067', '#FFD500', '#2DAAE1'],
},
{
title: 'MI_HAZANK',
sort: 'stable',
shade: '#678B1D',
},
];
export default operate SkinPicker() {
return (
Háttér kiválasztása
{skins.map(pores and skin => {
if (pores and skin.sort === 'stable') {
return (
setWidgetSkin(skin.name)}
/>
);
} else if (skin.type === 'gradient') {
return (
setWidgetSkin(skin.name)}
>
);
} else if (skin.type === 'dots') {
// Split box + 2 red dots
return (
setWidgetSkin(skin.name)}
>
{/* Red dots */}
);
}
})}
);
}
const styles = StyleSheet.create({
headerText: {
textAlign: 'center',
fontSize: 20,
marginBottom: 10,
},
row: {
flexDirection: 'row',
justifyContent: 'space-around',
gap: 10,
},
box: {
width: 50,
height: 50,
borderRadius: 8,
borderWidth: 1,
borderColor: '#333',
overflow: 'hidden',
},
dot: {
position: 'absolute',
width: 8,
height: 8,
borderRadius: 4,
},
});
RNWidgetModule.m
#import
@interface RCT_EXTERN_MODULE(RNWidgetModule, NSObject)
RCT_EXTERN_METHOD(setSkin:(NSString *)skinName)
RCT_EXTERN_METHOD(setCustomBackground:(NSString *)imageUri
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeCustomBackground)
RCT_EXTERN_METHOD(reloadWidgets)
@end
RNWidgetModule.swift
import Foundation
import React
import WidgetKit
import UIKit
@objc(RNWidgetModule)
class RNWidgetModule: NSObject {
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
@objc
func setSkin(_ skinName: String) {
DispatchQueue.main.async {
guard let userDefaults = UserDefaults(suiteName: "group.ittazido") else {
print("Failed to get UserDefaults with suite name")
return
}
userDefaults.set(skinName, forKey: "selectedSkin")
userDefaults.set(false, forKey: "isCustomBackground")
userDefaults.removeObject(forKey: "customBackgroundData")
WidgetCenter.shared.reloadAllTimelines()
userDefaults.synchronize()
print("Skin set to: (skinName)")
self.reloadWidgets()
}
}
@objc
func setCustomBackground(_ imageUri: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
DispatchQueue.main.async {
guard let url = URL(string: imageUri) else {
rejecter("INVALID_URI", "Invalid image URI", nil)
return
}
// Handle different URI schemes
var imageData: Data?
if imageUri.hasPrefix("ph://") {
// Photo library asset
self.loadPhotoLibraryAsset(url: url) { data in
if let data = data {
self.saveCustomBackground(data: data, resolver: resolver, rejecter: rejecter)
} else {
rejecter("LOAD_FAILED", "Failed to load image from photo library", nil)
}
}
return
} else if imageUri.hasPrefix("file://") {
// File system
let filePath = url.path
imageData = NSData(contentsOfFile: filePath) as Data?
} else if imageUri.hasPrefix("data:") {
// Base64 data URI
if let range = imageUri.range(of: ",") {
let base64String = String(imageUri[range.upperBound...])
imageData = Information(base64Encoded: base64String)
}
}
guard let knowledge = imageData else {
rejecter("LOAD_FAILED", "Did not load picture knowledge", nil)
return
}
self.saveCustomBackground(knowledge: knowledge, resolver: resolver, rejecter: rejecter)
}
}
personal func loadPhotoLibraryAsset(url: URL, completion: @escaping (Information?) -> Void) {
import Pictures
let assetId = url.absoluteString.replacingOccurrences(of: "ph://", with: "")
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], choices: nil)
guard let asset = fetchResult.firstObject else {
completion(nil)
return
}
let imageManager = PHImageManager.default()
let choices = PHImageRequestOptions()
choices.isSynchronous = false
choices.deliveryMode = .highQualityFormat
imageManager.requestImage(for: asset, targetSize: CGSize(width: 800, peak: 400), contentMode: .aspectFill, choices: choices) { picture, _ in
guard let picture = picture else {
completion(nil)
return
}
completion(picture.jpegData(compressionQuality: 0.8))
}
}
personal func saveCustomBackground(knowledge: Information, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
guard let userDefaults = UserDefaults(suiteName: "group.ittazido") else {
rejecter("USERDEFAULTS_ERROR", "Did not get UserDefaults", nil)
return
}
// Resize picture if wanted
guard let picture = UIImage(knowledge: knowledge) else {
rejecter("INVALID_IMAGE", "Invalid picture knowledge", nil)
return
}
let resizedImage = self.resizeImage(picture: picture, maxWidth: 800, maxHeight: 400)
guard let resizedData = resizedImage.jpegData(compressionQuality: 0.8) else {
rejecter("RESIZE_FAILED", "Did not resize picture", nil)
return
}
userDefaults.set(true, forKey: "isCustomBackground")
userDefaults.set(resizedData, forKey: "customBackgroundData")
userDefaults.synchronize()
self.reloadWidgets()
resolver("Customized background set efficiently")
}
@objc
func removeCustomBackground() {
DispatchQueue.important.async {
guard let userDefaults = UserDefaults(suiteName: "group.ittazido") else {
return
}
userDefaults.set(false, forKey: "isCustomBackground")
userDefaults.removeObject(forKey: "customBackgroundData")
userDefaults.synchronize()
self.reloadWidgets()
}
}
@objc
func reloadWidgets() {
DispatchQueue.important.async {
if #obtainable(iOS 14.0, *) {
WidgetCenter.shared.reloadAllTimelines()
print("Widgets reloaded")
}
}
}
personal func resizeImage(picture: UIImage, maxWidth: CGFloat, maxHeight: CGFloat) -> UIImage {
let measurement = picture.measurement
if measurement.width String! {
return "RNWidgetModule"
}
}
CountdownWidget.swift
import WidgetKit
import SwiftUI
import Basis
struct CountdownData {
let targetDate: Date
let lastFetchTime: Date
}
enum WidgetSkin: String, CaseIterable {
case fidesz = "FIDESZ"
case tisza = "TISZA"
case kutyapart = "KUTYAPART"
case dk = "DK"
case miHazank = "MI_HAZANK"
}
struct CountdownTimelineProvider: TimelineProvider {
typealias Entry = CountdownEntry
func placeholder(in context: Context) -> CountdownEntry {
CountdownEntry(date: Date(), targetDate: getDefaultTargetDate())
}
func getSnapshot(in context: Context, completion: @escaping (CountdownEntry) -> Void) {
let entry = CountdownEntry(date: Date(), targetDate: getTargetDate())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
fetchTargetDateFromAPI { targetDate in
var entries: [CountdownEntry] = []
let currentDate = Date()
// Create entries for each second for the subsequent minute
for secondOffset in 0.. Void) {
guard let url = URL(string: "https://api.lefiko.hu/time") else {
completion(getDefaultTargetDate())
return
}
URLSession.shared.dataTask(with: url) { knowledge, response, error in
guard let knowledge = knowledge,
let json = attempt? JSONSerialization.jsonObject(with: knowledge, choices: []) as? [String: Any],
let timeString = json["time"] as? String else {
completion(getDefaultTargetDate())
return
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd H:mm:ss"
formatter.timeZone = TimeZone(identifier: "Europe/Budapest")
if let date = formatter.date(from: timeString) {
// Cache the end result
UserDefaults(suiteName: "group.ittazido")?.set(date, forKey: "targetDate")
UserDefaults(suiteName: "group.ittazido")?.set(Date(), forKey: "lastFetchTime")
completion(date)
} else {
completion(getDefaultTargetDate())
}
}.resume()
}
personal func getTargetDate() -> Date {
guard let userDefaults = UserDefaults(suiteName: "group.ittazido"),
let cachedDate = userDefaults.object(forKey: "targetDate") as? Date,
let lastFetch = userDefaults.object(forKey: "lastFetchTime") as? Date,
Date().timeIntervalSince(lastFetch) Date {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd H:mm:ss"
formatter.timeZone = TimeZone(identifier: "Europe/Budapest")
return formatter.date(from: "2026-04-12 06:00:00") ?? Date()
}
}
struct CountdownEntry: TimelineEntry {
let date: Date
let targetDate: Date
}
struct CountdownWidgetEntryView: View {
var entry: CountdownEntry
@State personal var now = Date()
@AppStorage("selectedSkin", retailer: UserDefaults(suiteName: "group.ittazido"))
personal var selectedSkin: String = "DK"
@AppStorage("isCustomBackground", retailer: UserDefaults(suiteName: "group.ittazido"))
personal var isCustomBackground: Bool = false
personal var currentSkin: WidgetSkin {
WidgetSkin(rawValue: selectedSkin) ?? .dk
}
personal var customBackgroundData: Information? {
UserDefaults(suiteName: "group.ittazido")?.knowledge(forKey: "customBackgroundData")
}
var physique: some View {
ZStack {
// Background
if isCustomBackground, let imageData = customBackgroundData, let uiImage = UIImage(knowledge: imageData) {
Picture(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
getSkinBackground(for: currentSkin)
}
// Overlay purple dots for KUTYAPART
if currentSkin == .kutyapart {
VStack {
HStack {
Circle()
.fill(Colour.purple)
.body(width: 8, peak: 8)
Spacer()
}
Spacer()
HStack {
Spacer()
Circle()
.fill(Colour.purple)
.body(width: 8, peak: 8)
}
}
.padding(5)
}
// Countdown textual content
Textual content(getCountdownText())
.font(.system(measurement: 24, weight: .daring))
.foregroundColor(getTextColor(for: currentSkin))
.shadow(shade: .black.opacity(0.5), radius: 2, x: 1, y: 1)
.multilineTextAlignment(.heart)
}
.containerBackground(for: .widget) {
Colour.clear
}
}
personal func getCountdownText() -> String {
let now = entry.date
let goal = entry.targetDate
let diff = goal.timeIntervalSince(now)
if diff > 0 {
let days = Int(diff) / (24 * 60 * 60)
let hours = (Int(diff) % (24 * 60 * 60)) / (60 * 60)
let minutes = (Int(diff) % (60 * 60)) / 60
let seconds = Int(diff) % 60
return String(format: "%d nap %02d:%02d:%02d", days, hours, minutes, seconds)
} else if diff >= -24 * 60 * 60 {
return "ITT AZ IDŐ!"
} else {
return "4 év múlva újra találkozunk!"
}
}
personal func getSkinBackground(for pores and skin: WidgetSkin) -> some View {
Group {
swap pores and skin {
case .fidesz:
RoundedRectangle(cornerRadius: 8)
.fill(Colour(purple: 1.0, inexperienced: 0.416, blue: 0.075))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Colour.black.opacity(0.2), lineWidth: 1)
)
case .tisza:
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colours: [
Color(red: 0.141, green: 0.710, blue: 0.451),
Color(red: 0.929, green: 0.271, blue: 0.318)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Colour.black.opacity(0.2), lineWidth: 1)
)
case .kutyapart:
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colours: [Color.white, Color.black],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Colour.black.opacity(0.2), lineWidth: 1)
)
case .dk:
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colours: [
Color(red: 0.004, green: 0.384, blue: 0.655),
Color(red: 0.773, green: 0.000, blue: 0.404),
Color(red: 1.0, green: 0.835, blue: 0.0)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Colour.black.opacity(0.2), lineWidth: 1)
)
case .miHazank:
RoundedRectangle(cornerRadius: 8)
.fill(Colour(purple: 0.404, inexperienced: 0.545, blue: 0.114))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Colour.black.opacity(0.2), lineWidth: 1)
)
}
}
}
personal func getTextColor(for pores and skin: WidgetSkin) -> Colour {
swap pores and skin {
case .kutyapart:
return .black
default:
return .white
}
}
}
struct CountdownWidget: Widget {
let variety: String = "CountdownWidget"
var physique: some WidgetConfiguration {
StaticConfiguration(variety: variety, supplier: CountdownTimelineProvider()) { entry in
CountdownWidgetEntryView(entry: entry)
}
.configurationDisplayName("Countdown Widget")
.description("Shows a countdown to the goal date.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
#Preview(as: .systemSmall) {
CountdownWidget()
} timeline: {
let targetDate = Calendar.present.date(byAdding: .day, worth: 30, to: Date()) ?? Date()
CountdownEntry(date: .now, targetDate: targetDate)
}