SwiftData and command line

Is it possible to use SwiftData in a CLI tool, or is it only designed to work with SwiftUI?

Replies

yes. they give u sample code to setup the stack in swift without using swiftui

I clearly missed that!

I still can't find it. 😩

I found some sample code elsewhere, and -- unless I'm missing something, which is quite likely -- it will not work with a CLI tool, because it needs to know the bundle name.

  • Where did you found this sample code? Can you please give us the link?

Add a Comment

I don’t have any experience with the SwiftData side of this but:

it needs to know the bundle name.

it is possible to give a command-line tool a bundle ID, bundle name, and so on. The trick is to embed the Info.plist into a special section in your tool. See the Create Info.plist Section in Binary (CREATE_INFOPLIST_SECTION_IN_BINARY) build setting.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I was so tired this weekend that I couldn't go into that; I know it's possible (using it at work for a couple of daemons), but I just couldn't think. 😄

I was trying to make a little CLI utility that could look at the data a SwiftUI app dealt with.

I'm not sure how to use CREATE_INFOPLIST_SECTION_IN_BINARY with SPM, but I've found that adding linkerSettings to the Platform.swift file works well.

            linkerSettings: [
                .unsafeFlags([
                    "-Xlinker", "-sectcreate",
                    "-Xlinker", "__TEXT",
                    "-Xlinker", "__info_plist",
                    "-Xlinker", "Sources/Builder/Info.plist",
                ])
            ]

I'm not sure how to use CREATE_INFOPLIST_SECTION_IN_BINARY with SwiftPM

You can’t. That’s an Xcode build setting.

but I've found that adding linkerSettings to the Platform.swift file works well.

Yep. That’s equivalent to what Xcode does under the covers when you apply that build setting.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

It would be great to have even the most bare-bone sample code that illustrates this concept for the CLI template.

The build setting for the Info.plist is findable and thanks so far for the pointer to that, but it's only the first step. For example, if you take the default @Model from the app template and want to just inspect the context:

import Foundation
import SwiftData

@Model
final class Item {
    var timestamp: Date = Date()
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

let bundleIdentifier = Bundle.main.bundleIdentifier
let container = try ModelContainer(for: Item.self)

Task {
    
    let context = await container.mainContext
    print(context.autosaveEnabled) // or actually something useful from the ModelContext like real data

}

This doesn't compile (and is likely architected horribly) and while I'm sure the ModelConfiguration documentation is the right place to start, it's too opaque to string together.

Understandable that not everyone wants to use SwiftData this way, but it seems odd that if I want to propagate some test data to a container I have to make up a UI-based app where I don't want the UI.

It would be great to have even the most bare-bone sample code that illustrates this concept for the CLI template.

Sadly, I don’t have time to do a full end-to-end test with this today. However, I can help you with your compilation errors.

This doesn't compile

Yes.

(and is likely architected horribly)

And yes (-;

When building command-line tools it’s best not to put a lot of code at the top level. Rather, wrap it in a main() function and call that. For example:

func main() {
    … your code here …
}

main()

That’s because Swift has some pretty funky rules when it comes to global variables defined in your main source file.

Alternatively, you can declare your own main type, like so:

@main
struct Main {
    static func main() async throws {
        … your code here …
    }
}

IMPORTANT For this to work, rename your file to something other than main.swift. I typically use start.swift, but anything will work as long as it’s not main.swift.

This is how, for example, Swift Argument Parser works.

That automatically puts you on the main actor, and from there you run your main actor and async code:

… your SwiftData stuff …

@main
struct Main {
    static func main() async throws {
        let container = try ModelContainer(for: Item.self)

        let context = container.mainContext
        print(context.autosaveEnabled)
    }
}

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

This is great, thanks so much. It will solve my immediate needs and points me to the next right thing to get up to speed on (Actors in general, but this is the second time I have specifically heard about the 'main Actor' so I've got to brush up there).

import Foundation
import SwiftData

@Model class Person {
  var name: String
  
  init(name: String) {
    self.name = name
  }
}

func main() {
  print(Bundle.main.bundleIdentifier ?? "")                             //  com.northbronson.DataDemo
  print(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") ?? "") //  DataDemo
  
  let configuration = ModelConfiguration(
    isStoredInMemoryOnly: true,
    allowsSave: false
  )
  
  print(configuration.url)                                              //  file:///dev/null
  
  //  let _ = configuration.url.startAccessingSecurityScopedResource()
  
  do {
    let _ = try ModelContainer(
        for: Person.self,
        configurations: configuration
    )
  } catch {
    //  configuration.url.stopAccessingSecurityScopedResource()
    
    print(error)
  }
}

main()

//  com.northbronson.DataDemo
//  DataDemo
//  file:///dev/null
//  error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (257)
//  CoreData: error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (257)
//  error: userInfo:
//  CoreData: error: userInfo:
//  error:   NSFilePath : /dev/null
//  CoreData: error:   NSFilePath : /dev/null
//  error: storeType: SQLite
//  CoreData: error: storeType: SQLite
//  error: configuration: (null)
//  CoreData: error: configuration: (null)
//  error: URL: file:///dev/null/
//  CoreData: error: URL: file:///dev/null/
//  CoreData: annotation: options:
//  CoreData: annotation:   NSMigratePersistentStoresAutomaticallyOption : 1
//  CoreData: annotation:   NSPersistentHistoryTrackingKey : 1
//  CoreData: annotation:   NSInferMappingModelAutomaticallyOption : 1
//  CoreData: annotation:   NSPersistentStoreRemoteChangeNotificationOptionKey : 1
//  CoreData: annotation:   NSReadOnlyPersistentStoreOption : 1
//  error: <NSPersistentStoreCoordinator: 0x600000960180>: Attempting recovery from error encountered during addPersistentStore: 0x600002960f00 Error Domain=NSCocoaErrorDomain Code=257 "The file “null” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/dev/null}
//  CoreData: error: <NSPersistentStoreCoordinator: 0x600000960180>: Attempting recovery from error encountered during addPersistentStore: 0x600002960f00 Error Domain=NSCocoaErrorDomain Code=257 "The file “null” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/dev/null}
//  error: Store failed to load.  <NSPersistentStoreDescription: 0x600002960db0> (type: SQLite, url: file:///dev/null/) with error = Error Domain=NSCocoaErrorDomain Code=257 "The file “null” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/dev/null} with userInfo {
//      NSFilePath = "/dev/null";
//  }
//  CoreData: error: Store failed to load.  <NSPersistentStoreDescription: 0x600002960db0> (type: SQLite, url: file:///dev/null/) with error = Error Domain=NSCocoaErrorDomain Code=257 "The file “null” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/dev/null} with userInfo {
//      NSFilePath = "/dev/null";
//  }
//  Unresolved error loading container Error Domain=NSCocoaErrorDomain Code=257 "The file “null” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/dev/null}
//  SwiftDataError(_error: SwiftData.SwiftDataError._Error.loadIssueModelContainer)
//  Program ended with exit code: 0

Was anyone able to get this working with a ModelConfiguration set to isStoredInMemoryOnly? I followed the steps to add CREATE_INFOPLIST_SECTION_IN_BINARY and I see the values being read correctly from Bundle… then this error throws up… I'm trying the startAccessingSecurityScopedResource that I read about… no luck. Hmm… AFAIK this is just a plain vanilla macOS command line tool (other than injecting an info.plist) with no funky sandbox behavior going on. Any ideas what else to try or where else to look?

import Foundation
import SwiftData

@Model class Person {
  var name: String
  
  init(name: String) {
    self.name = name
  }
}

func main() {
  let configuration = ModelConfiguration(
    isStoredInMemoryOnly: true,
    allowsSave: true
  )
  
  do {
    let _ = try ModelContainer(
        for: Person.self,
        configurations: configuration
    )
  } catch {
    print(error)
  }
}

main()

I'm able to build and run by passing true to allowsSave. Is passing false from CLI not supported?

I'm on Xcode Version 15.2 (15C500b).