gitn00b1337 / expo-widgets

Bringing widget functionality to expo!
145 stars 12 forks source link

Pass nested object props #9

Closed romainchkr closed 8 months ago

romainchkr commented 8 months ago

Hello,

First thank you for that amazing package. it works great. Right now i'm trying to pass nested object data from react to the widget (ios only) but its seems quite challenging.

I started from your example wich works great and I tried to add a list of Message object to the myData struct.

I have the following MyData.swift :

// Mydata.swift
public struct MyMessage: Codable {
    var name: String
    var avatar: String
    var message: String
    var isMe: Bool
}

public struct MyData: Codable {
    var groupName: String
    var backgroundImage: String
    var messages: [MyMessage]
    var type: String
}

The following Module.swift :

import ExpoModulesCore
import ActivityKit
import WidgetKit

struct SetDataProps: Record {
  @Field
  var groupName: String
  @Field
  var backgroundImage: String
  @Field
  var messages: [MessageProp]
  @Field
  var type: String
}

struct MessageProp: Record {
    @Field 
    var name: String
    @Field 
    var avatar: String
    @Field 
    var message: String
    @Field 
    var isMe: Bool
}

// the module is the message handler between your app and the widget
// you MUST declare the name of the module and then write whatever functions
// you wish your app to send over
public class ExpoWidgetsModule: Module {
    public func definition() -> ModuleDefinition {
        Name("ExpoWidgets")

        Function("setWidgetData") { (props: SetDataProps) -> Void in   
            let logger = Logger()        
            do {
                let myData = MyData(
                  groupName: props.groupName,
                  backgroundImage: props.backgroundImage,
                  messages: props.messages.map { MyMessage(name: $0.name, avatar: $0.avatar, message: $0.message, isMe: $0.isMe) },
                  type: props.type
              )

              // here we are using UserDefaults to send data to the widget
              // you MUST use a suite name of the format group.{your project bundle id}.expowidgets
              let encoder = JSONEncoder()
              let data = try encoder.encode(myData)
              let widgetSuite = UserDefaults(suiteName: "group.com.romain.privatewidget.expowidgets")
              widgetSuite?.set(data, forKey: "MyData")
              logger.info("Encoded data saved to key MyData")

              // this is optional, but while your app is open and in focus
              // messages sent to the widget do not count towards its timeline limitations
              if #available(iOS 14.0, *) {
                    WidgetCenter.shared.reloadAllTimelines()
                }
            } 
            catch (let error) {
                logger.info("An error occured setting MyData!")
            }
        }
    }
}

And i decode it like that in swift :

// Entry
struct Entry: TimelineEntry {
    var date: Date = .now
    var groupName: String = ""
    var backgroundImage: String = ""
    var messages: [Message] = []
    var type: String = "disconnected"
    var image: Loadable<Image>
}

struct Message: Identifiable {
    var id = UUID()
    var name: String
    var avatar: String
    var message: String
    var isMe: Bool
}

func getEntry(context: Context) -> MyWidget.Entry {
        let widgetSuite = UserDefaults(suiteName: "group.com.romain.privatewidget.expowidgets")

        if let data = widgetSuite?.data(forKey: "MyData") {
            do {
                print("decode data")
                let decoder = JSONDecoder()
                let decodedData = try decoder.decode(MyData.self, from: data)
                let entry = Entry(
                    date: Date(),
                    groupName: decodedData.groupName,
                    backgroundImage: decodedData.backgroundImage,
                    // messages: decodedData.messages
                    messages: [],
                    type: decodedData.type,
                    image: Loadable<Image>.notRequested
                )

                return entry
            } catch {
               ...
            }
        }

       ....
    }

With this code i face the following issue :

❌  (ios/privatewidgetWidgetExtension/MyWidget.swift:16:39)

  14 |                 groupName: decodedData.groupName,
  15 |                 backgroundImage: decodedData.backgroundImage,
> 16 |                 messages: decodedData.messages
     |                                       ^ cannot convert value of type '[MyMessage]' to expected argument type '[Message]'
  17 |             )
  18 |             
  19 |             return entry

I'm not really familiar to swift and I tried different things but I face all the time issues. I'm wondering if you have any clue ?

Thank you.

gitn00b1337 commented 8 months ago

I've been having issues with the module code being used, expo changed somewhere between updates and it broke. To check, remove your module file (put it elsewhere as a backup) and try passing the correct model directly from javascript.

gitn00b1337 commented 8 months ago

I've been having issues with the module code being used, expo changed somewhere between updates and it broke. To check, remove your module file (put it elsewhere as a backup) and try passing the correct model directly from javascript.

romainchkr commented 8 months ago

You are right I didn't notice but passing data from react native to the swift widget is not working anymore.

What do you mean by

removing the module file and try passing the correct model directly from javascript

For the moment i'm passing data from RN to Swift using the code inside the module file (i i understand good) and calling the following in RN :

import * as ExpoWidgetsModule from '@bittingz/expo-widgets';
ExpoWidgetsModule.setWidgetData({groupName: 'disconnected', backgroundImage: '', messages: [], type: 'disconnected'});

I don't see how I should do without the module file.

Even tho I tried to delete the Module.swift and launched the app but it doesn't seems to work. The data is still not passing from RN to the IOS widget.

Do you have a workaround ?

gitn00b1337 commented 8 months ago

The library used to allow you to provide a module file but expo changed something and it stopped working.

Instead, it just uses the libraries module file. So if you look at the example project, it just sends an object from js that has the same format as the swift object. It's swapped over by json. Make sure your key is correct if you don't get any data. If you can pass a simple object of one field correctly but it doesn't work for nested then I'll investigate. For my project I only have no nesting in my object so tbh I haven't considered this yet. But I have many properties passed.

One key point is making sure the json for messages works. If you have certain renames between your react native JavaScript and the swift widget, either change them to match or map them before sending over.

romainchkr commented 8 months ago

Okay i will try with a simple object then.

If I understand good what I should do is the following:

  1. Remove the Module.swift file (in order to use the Module file from the library)
  2. Pass data from RN to the IOS Widget using the following syntax :
    var data = { groupName: 'my group',  backgroundImage: 'https://....'};
    ExpoWidgetsModule.setWidgetData(JSON.stringify(data));

having the following MyData structure in swift :

public struct MyData: Codable {
    var groupName: String
    var backgroundImage: String
}
  1. If it works with simple variables, try the nested objects in a json format
romainchkr commented 8 months ago

It seems impossible to compile the app without any Module.swift file in the widgets/ios folder.

romainchkr commented 8 months ago

I finally managed.

Here is how i did for anyone interested :

// MyData.swift
public struct MyMessage: Codable {
    var name: String
    var message: String
    var isMe: Bool
}

public struct MyData: Codable {
    var groupName: String
    var backgroundImage: String
    var messages: [MyMessage]
    var type: String
    var nbFire: Int
}
// Module.swift
import ExpoModulesCore
import ActivityKit
import WidgetKit

public class ExpoWidgetsModule: Module {
    public func definition() -> ModuleDefinition {
        Name("ExpoWidgets")

        Function("setWidgetData") { (data: String) -> Void in   
            let logger = Logger()        
            do {
              logger.info("!! setWidgetData");
              let widgetSuite = UserDefaults(suiteName: "group.com.myapp.expowidgets")
              widgetSuite?.set(data, forKey: "MyData")
              logger.info("!! Encoded data saved to key MyData")
              if #available(iOS 14.0, *) {
                    WidgetCenter.shared.reloadAllTimelines()
                }
            } 
            catch (let error) {
                logger.info("An error occured setting MyData!")
            }
        }
    }
}
// Entries
struct Entry: TimelineEntry {
      var date: Date = .now
      var groupName: String = ""
      var backgroundImage: String = ""
      var messages: [Message] = []
      var type: String = "disconnected"
      var nbFire: Int = 0
      var image: Loadable<Image>
  }

  struct Message: Identifiable {
      var id = UUID()
      var name: String
      var message: String
      var isMe: Bool
  }
// How i retrieve the data from the userdefault
let widgetSuite = UserDefaults(suiteName: "group.com.rocketapp.privatewidget2.expowidgets")  

if let jsonData = widgetSuite?.string(forKey: "MyData") {
    do {
        if let data = jsonData.data(using: .utf8) {
            let widgetData = try JSONDecoder().decode(MyData.self, from: data)

            let entry = Entry(
                date: Date(),
                groupName: widgetData.groupName,
                backgroundImage: widgetData.backgroundImage,
                messages: widgetData.messages.map { Message(name: $0.name, message: $0.message, isMe: $0.isMe) },
                type: widgetData.type,
                nbFire: widgetData.nbFire,
                image: Loadable<Image>.notRequested
            )
            return entry
        } else {
            ...
        }                
    } catch {
       ...
    }
}