Where and How to Include File Resources Within IOS component of Flutter Plugin?
Asked Answered
R

2

7

Let's say you're writing the IOS component of a Flutter plugin in Swift.

You have your MyFlutterPlugin.swift file open in XCode.

So that we know what we're talking about the same thing, it's the file that looks like this:

public class MyFlutterPlugin: NSObject, FlutterPlugin {

    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "my_flutter_plugin", binaryMessenger: registrar.messenger())
        let instance = SwiftMetronomeFlutterPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {

    // ...

    }

}

This file exists on disk at:

[your_plugin_project_name]/ios/classes/MyFlutterPlugin.swift

How could you add a resource, for example an audio (WAV) file, such that it could be referenced from within MyFlutterPlugin.swift?

In a normal (not Flutter) IOS app, I have accomplished this by simply dragging the WAV file into XCode where it sits alongside ViewController.swift (in the same folder), and referenced it from within the ViewController like this:

let clack_Path = Bundle.main.path(forResource: "clack", ofType: "wav")
let clack_URL = URL(fileURLWithPath: clack_Path!)

But, if I try that same approach from within MyFlutterPlugin.swift (putting the audio file alongside MyFlutterPlugin.swift), I get an error that clack_Path is null.

So where is the correct place to put the file, and how to properly reference it?

I can't find any code examples, tutorials, or documentation (at least not any that I recognize as being useful) on this specific problem. Hopefully it's actually possible to do on IOS... I've already got the resources included and working on the Android side of the plugin.

Reg answered 10/1, 2022 at 4:32 Comment(4)
If you open the project in Xcode (not the plugin project, the project using the plugin) do you see the file? If so, where is it? If you want to google a bit yourself, you can search how to add a file to a cocoapod pod. Maybe this link wil help: https://mcmap.net/q/1480866/-how-do-i-create-a-cocoapod-framework-and-add-files-to-itShaylyn
does my answer solve your problem?Mounts
Michele Volpato, I can't totally answer that question because XCode seems to open the same project location either way (example vs plugin), but the file does show up where I placed it as described in the question. But then again, as described in question, I don't know if that is the correct location. Thanks for the link, but as soon as that link mentioned "private pod" and "module" I am lost. Can someone please just answer the question as asked it would be much appreciated. All details in question are accurate.Reg
ch271828, I'm hoping for an explicit, step-by-step answer to my question rather than a suggestion of a different approach or pointing to more general documentation. I tried reading that that documentation but it's not working out. My brain doesn't want to go in that direction and I'm hitting snags, for example, on the Android code example, the "registrar" instance doesn't exist in my code. In their example code, they do not show how they constructed the "registrar" variable and when I looked up PluginRegistry.Registrar, it is an abstract class so I don't know what I'm supposed to do with that.Reg
A
8

To be able to use assets in the Flutter Plugin, firstly, we need to add the resources inside the plugins's folder, in this example, I am placing the clap.wav file in the folder [your_plugin_project_name]/ios/Assets/.

After adding the file inside the plugin's folder, we need to specify in the plugin's PodSpec where the assets are located.

# File [your_plugin_project_name]/my_flutter_plugin.podspec
Pod::Spec.new do |s|
  # [...] supressed content above
  s.source_files = 'Classes/**/*'
  s.resources    = ['Assets/**.*']
  # [...] supressed content below
end

The important part in the snippet above is the line s.resources = ['Assets/**.*'], where Assets/**/* specify that all the plugin's resources are inside the folder Assets. The Assets/**/* is a wildcard used to tell that every file (the * string part) in every folder/subfolder (the ** part) within the folder Assets must be included as resources of the plugin's bundle. You can learn more about this kind of string searching about regular expression.

Every time you change the plugin's configuration, you'll need to inform the Flutter's project using the plugin that there are new changes. In a regular project, you would need to bump the plugin's version and release a new version of it in the Pub, but as we are just changing this locally (without releasing it to the world), we need only to inform Flutter that the plugin's files are outdated. The easiest way to do that is to run flutter clean inside the project that is using the plugin.

After that, you will be able to access the clap.wav file in the plugins's swift files using the code:

guard let url = Bundle(for: type(of: self)).url(forResource: "clap", withExtension: "wav") else {
   return print("File not found") 
}

Notice that you must use Bundle(for: type(of: self)) instead of Bundle.main (the Bundle.main points to the Runner's bundle instead of your plugin's bundle).

I the example below, I show how you could play the clap.wav using swift code:

import Flutter
import UIKit
import AVFoundation

public class SwiftMyFlutterPlugin: NSObject, FlutterPlugin {
  private var player: AVAudioPlayer?

  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "my_flutter_plugin", binaryMessenger: registrar.messenger())
    let instance = SwiftMyFlutterPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "getPlatformVersion":
      return result("iOS " + UIDevice.current.systemVersion)
    case "playSound":
      return playSound(result: result)
    default:
      return result(FlutterMethodNotImplemented)
    }
  }

  private func playSound(result: @escaping FlutterResult) {
    if player?.isPlaying == true { return }

    let filename = "clap"
    let fileExtension = "wav"
    guard let url = Bundle(for: type(of: self)).url(forResource: filename, withExtension: fileExtension) else {
      let flutterError = FlutterError(
        code: "fileNotFound",
        message: "File not found: \(filename).\(fileExtension).",
        details: nil
      )
      return result(flutterError)
    }

    do {
      try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
      try AVAudioSession.sharedInstance().setActive(true)
    } catch {
      let flutterError = FlutterError(
        code: "audioSessionSetupError",
        message: "Error on AVAudionSession setup.",
        details: error.localizedDescription
      )
      return result(flutterError)
    }

    do {
      player = try AVAudioPlayer(contentsOf: url)
      player?.play()
    } catch {
      let flutterError = FlutterError(
        code: "audioPlayerSetupError",
        message: "Error on AVAudioPlayer setup.",
        details: error.localizedDescription
      )
      return result(flutterError)
    }
  }
}

The example above is a simple example of how you could play the clap.wav file, but you could do in different other ways. As this is not related with the question, I think that the example above is enough to answer your question.

To facilitate your understanding and easily demonstrate that the code above works, I created a simple project in the GitHub that you can execute to see that this works as expected, see it here.

Notice that you must run the code inside the folder example.

Afb answered 18/1, 2022 at 17:29 Comment(9)
Thanks, this looks promising! However is there a way we somehow can initialize the url as a class-level variable or as a one-time initialization instead of doing it inside the handle() method? I've noticed I can only use that code inside the handle method, otherwise there are errors. I have other internal classes that need the resource as a constructor argument and I don't want to have to run the init code every time a platform call comes through.Reg
The important part is adding the resource in the ‘podspec’ file of the plugin and use ‘Bundle(for: type(of: self))’ to access the asset in the Swift code. Where and how you will use the assets is not related with Flutter and you can do however you want. I only provided a simple example use.Afb
Unfortunately this isn't working. When I made my prior comment I had not built/run the code but was happy that there were no errors in the IDE. I'm getting url = nil at runtime (file not found) even though the file exists in both the classes dir and the assets dir. Should I have written "s.resources = ['Assets/*.wav']" exactly or did you intend that I replace the * with "clack" ? Did you test this before answering? I tried a) removing brackets, b) replacing * with "clack" and many many other small changes to the podspec file syntax and to the swift code.Reg
I noticed that if I intentionally muck up pertinent line in the podspec file to "s.resources = #210823427642765?>!!nutsack" it still builds just fine... and ends up with the same error... so either it is not paying any attention to the file that I'm editting or I have never defined the file location with the correct syntax.Reg
Did you try to run pod install or flutter clean after changing the .podspec file? If not, the plugin bundle will not reflect the changes in the podspec file.Afb
I don't know what directory I should be in when running "pod install," so instead, I used flutter clean from Android Studio while having the plugin project open, but same result unfortunately.Reg
Run the code flutter clean inside the root folder of the project that is using the plugin. After that, run the project that is using the plugin. I updated the question with more details and also pointed out to a Github repository that shows this working.Afb
Thankyou for making the github repo. I verified that your project builds and runs. I was then able to determine what was needed in my project: 1) open terminal at [my_plugin_project]/example/ios 2) run "pod install". This is what was preventing the podspec file from being applied. I had assumed that running "flutter clean" would also run "pod install" by default but that was apparently incorrect. I know that you already explained this but it was not clear since my ios/cocoapods knowledge is extremely limited. Thanks again!Reg
I hadn't any problem using Bundle.main. I was able to create a UIImage with: path = Bundle.main.path(forResource: "...", ofType: "png") and then UIImage(contentsOfFile: path)Indebtedness
M
2

Try this: https://docs.flutter.dev/development/ui/assets-and-images#loading-flutter-assets-in-ios

In short, firstly, put your file as a normal Flutter asset (instead of ios/android asset). Then, use the approach in the link to access it.

Mounts answered 14/1, 2022 at 0:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.