SwiftFM is a Swift Package for the FileMaker Data API. It uses modern Swift features like async/await
, Codable
type-safe returns, and has extensive support for DocC
.
This README.md
is aimed at Swift devs who want to use the Data API in their UIKit and SwiftUI projects. Each function shown below is paired with a code example.
SwiftFM is in no way related to the FIleMaker iOS App SDK.
- Xcode -> File -> Add Packages
https://github.com/starsite/SwiftFM.git
- UIKit: Set your enivronment in
applicationWillEnterForeground(_:)
- SwiftUI: Set your enivronment in
MyApp.init()
- Add an
import SwiftFM
statement - Call
SwiftFM.newSession()
and get a token ✨ - Woot!
If you'd like to support the SwiftFM project, you can:
- Contribute socially, by giving SwiftFM a ⭐️ on GitHub or telling other people about it
- Contribute financially (paypal.me/starsite)
- Hire me to build an iOS app for you or one of your FileMaker clients. 🥰
SwiftFM was rewritten last year to use async/await
. This requires Swift 5.5 and iOS 15. If you need to compile for iOS 13 or 14, skip SPM and download the repo instead, and convert the URLSession
calls using withCheckedContinuation
. For more information on that, visit: Swift by Sundell, Hacking With Swift, or watch Apple's WWDC 2021 session on the topic.
environment variables
newSession()
validateSession(token:)
deleteSession(token:)
createRecord(layout:payload:token:)
duplicateRecord(id:layout:token:)
editRecord(id:layout:payload:token:)
deleteRecord(id:layout:token:)
query(layout:payload:token:)
getRecords(layout:limit:sortField:ascending:portal:token:)
getRecord(id:layout:token:)
setGlobals(payload:token:)
getProductInfo()
getDatabases()
getLayouts(token:)
getLayoutMetaData(layout:token:)
getScripts(token:)
executeScript(script:parameter:layout:token:)
setContainer(recordId:layout:container:filePath:inferType:token:)
For TESTING, you can set these with string literals. For PRODUCTION, you should be getting these values from elsewhere. DO NOT deploy apps with credentials visible in code. 😵
Set your environment in AppDelegate
inside applicationWillEnterForeground(_:)
.
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
func applicationWillEnterForeground(_ application: UIApplication) {
let host = "my.host.com" //
let db = "my_database" //
// fetch these from elsewhere or prompt at launch
let user = "username" //
let pass = "password" //
UserDefaults.standard.set(host, forKey: "fm-host")
UserDefaults.standard.set(db, forKey: "fm-db")
let str = "\(user):\(pass)"
if let auth = str.data(using: .utf8)?.base64EncodedString() {
UserDefaults.standard.set(auth, forKey: "fm-auth")
}
}
// ...
}
Set your environment in MyApp: App
. If you don't see an init()
function, add one and finish it out like this.
@main
struct MyApp: App {
init() {
let host = "my.host.com" //
let db = "my_database" //
// fetch these from elsewhere or prompt at launch
let user = "username" //
let pass = "password" //
UserDefaults.standard.set(host, forKey: "fm-host")
UserDefaults.standard.set(db, forKey: "fm-db")
let str = "\(user):\(pass)"
if let auth = str.data(using: .utf8)?.base64EncodedString() {
UserDefaults.standard.set(auth, forKey: "fm-auth")
}
}
var body: some Scene {
// ...
}
}
Returns an optional token
.
If this fails due to an incorrect Authorization
, the FileMaker Data API will return an error code
and message
to the console. All SwiftFM calls output a simple success or failure message.
func newSession() async -> String? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let auth = UserDefaults.standard.string(forKey: "fm-auth"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/sessions")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Basic \(auth)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMSession.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
guard let token = result.response.token else { return nil }
UserDefaults.standard.set(token, forKey: "fm-token")
print("✨ new token » \(token)")
return token
default:
print(message)
return nil
}
}
if let token = await SwiftFM.newSession() {
print("✨ new token » \(token)")
}
FileMaker Data API 19 or later. Returns a Bool
. This function isn't all that useful on its own. But you can use it to wrap other calls to ensure they're fired with a valid token
.
func validateSession(token: String) async -> Bool {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/validateSession")
else { return false }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMSession.self, from: data),
let message = result.messages.first
else { return false }
// return
switch message.code {
case "0":
print("✅ valid token » \(token)")
return true
default:
print(message)
return false
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let isValid = await SwiftFM.validateSession(token: token)
switch isValid {
case true:
fetchArtists(token: token)
case false:
if let newToken = await SwiftFM.newSession() {
fetchArtists(token: newToken)
}
}
Returns a Bool
. For standard Swift (UIKit) apps, a good place to call this would be applicationDidEnterBackground(_:)
. For SwiftUI apps, you should call it inside a \.scenePhase.background
switch.
FileMaker's Data API has a 500-session limit, so managing session tokens will be important for larger deployments. If you don't delete your session token, it will should expire 15 minutes after the last API call. Probably. But you should clean up after yourself and not assume this will happen. 🙂
func deleteSession(token: String, completion: @escaping (Bool) -> Void) {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/sessions/\(token)")
else { return }
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
URLSession.shared.dataTask(with: request) { data, resp, error in
guard let data = data, error == nil,
let result = try? JSONDecoder().decode(FMSession.self, from: data),
let message = result.messages.first
else { return }
// return
switch message.code {
case "0":
UserDefaults.standard.set(nil, forKey: "fm-token")
print("🔥 deleted token » \(token)")
completion(true)
default:
print(message)
completion(false)
}
}.resume()
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
func applicationDidEnterBackground(_ application: UIApplication) {
if let token = UserDefaults.standard.string(forKey: "fm-token") {
SwiftFM.deleteSession(token: token) { _ in }
}
}
// ...
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
DispatchQueue.global(qos: .background).async { // extra time
if let token = UserDefaults.standard.string(forKey: "fm-token") {
SwiftFM.deleteSession(token: token) { _ in }
}
}
default: break
}
}
} // .body
}
Returns an optional recordId
. This can be called with or without a payload. If you set a nil
payload, a new empty record will be created. Either method will return a recordId
. Set your payload with a [String: Any]
object containing a fieldData
key.
func createRecord(layout: String, payload: [String: Any]?, token: String) async -> String? {
var fieldData: [String: Any] = ["fieldData": [:]] // nil payload
if let payload { // non-nil payload
fieldData = payload
}
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records"),
let body = try? JSONSerialization.data(withJSONObject: fieldData)
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = body
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMRecord.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
guard let recordId = result.response.recordId else { return nil }
print("✨ new recordId: \(recordId)")
return recordId
default:
print(message)
return nil
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"
let payload = ["fieldData": [ // required key
"firstName": "Brian",
"lastName": "Hamm",
"email": "hello@starsite.co"
]]
if let recordId = await SwiftFM.createRecord(layout: layout, payload: payload, token: token) {
print("created record: \(recordId)")
}
FileMaker Data API 18 or later. Pretty simple call. Returns an optional recordId
for the new record.
func duplicateRecord(id: Int, layout: String, token: String) async -> String? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMRecord.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
guard let recordId = result.response.recordId else { return nil }
print("✨ new recordId: \(recordId)")
return recordId
default:
print(message)
return nil
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"
if let recordId = await SwiftFM.duplicateRecord(id: recid, layout: layout, token: token) {
print("new record: \(recordId)")
}
Returns an optional modId
. Pass a [String: Any]
object with a fieldData
key containing the fields you want to modify.
modId
value in your payload
(from say, an earlier fetch), the record will only be modified if the modId
matches the value on FileMaker Server. This ensures you're working with the current version of the record. If you do not pass a modId
, your changes will be applied without this check.
Note: The FileMaker Data API does not pass back a modified record object for you to use. So you might want to refetch the updated record afterward with getRecord(id:)
.
func editRecord(id: Int, layout: String, payload: [String: Any], token: String) async -> String? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)"),
let body = try? JSONSerialization.data(withJSONObject: payload)
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "PATCH"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = body
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMRecord.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
guard let modId = result.response.modId else { return nil }
print("updated modId: \(modId)")
return modId
default:
print(message)
return nil
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"
let payload = ["fieldData": [
"address": "My updated address",
]]
if let modId = await SwiftFM.editRecord(id: recid, layout: layout, payload: payload, token: token) {
print("updated modId: \(modId)")
}
Pretty self explanatory. Returns a Bool
.
func deleteRecord(id: Int, layout: String, token: String) async -> Bool {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
else { return false }
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.first
else { return false }
// return
switch message.code {
case "0":
print("deleted recordId: \(id)")
return true
default:
print(message)
return false
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"
let result = await SwiftFM.deleteRecord(id: recid, layout: layout, token: token)
if result == true {
print("deleted recordId \(recordId)")
}
Returns a record
array and dataInfo
response. This is our first function that returns a tuple. You can use either object (or both). The dataInfo
object includes metadata about the request (database, layout, and table; as well as record count values for total, found, and returned). If you want to ignore dataInfo
, you can assign it an underscore.
You can set your payload
from the UI, or hardcode a query. Then pass it as a [String: Any]
object with a query
key.
func query(layout: String, payload: [String: Any], token: String) async throws -> (Data, FMResult.DataInfo) {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/_find"),
let body = try? JSONSerialization.data(withJSONObject: payload)
else { throw FMError.jsonSerialization }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = body
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
let response = json["response"] as? [String: Any],
let messages = json["messages"] as? [[String: Any]],
let message = messages[0]["message"] as? String,
let code = messages[0]["code"] as? String
else { throw FMError.sessionResponse }
// return
switch code {
case "0":
guard let data = response["data"] as? [[String: Any]],
let records = try? JSONSerialization.data(withJSONObject: data),
let dataInfo = result.response.dataInfo
else { throw FMError.jsonSerialization }
print("fetched \(dataInfo.foundCount) records")
return (records, dataInfo)
default:
print(message)
throw FMError.nonZeroCode
}
}
Note the difference in payload between an "or" request vs. an "and" request.
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"
// find artists named Brian or Geoff
let payload = ["query": [
["firstName": "Brian"],
["firstName": "Geoff"]
]]
// find artists named Brian in Dallas
let payload = ["query": [
["firstName": "Brian", "city": "Dallas"]
]]
guard let (data, _) = try? await SwiftFM.query(layout: layout, payload: payload, token: token),
let records = try? JSONDecoder().decode([Artist].self, from: data)
else { return }
self.artists = records // set @State data source
Returns a record
array and dataInfo
response. All SwiftFM record fetching methods return a tuple.
func getRecords(layout: String,
limit: Int,
sortField: String,
ascending: Bool,
portal: String?,
token: String) async throws -> (Data, FMResult.DataInfo) {
// param str
let order = ascending ? "ascend" : "descend"
let sortJson = """
[{"fieldName":"\(sortField)","sortOrder":"\(order)"}]
"""
var portalJson = "[]" // nil portal
if let portal { // non-nil portal
portalJson = """
["\(portal)"]
"""
}
// encoding
guard let sortEnc = sortJson.urlEncoded,
let portalEnc = portalJson.urlEncoded,
let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/?_limit=\(limit)&_sort=\(sortEnc)&portal=\(portalEnc)")
else { throw FMError.urlEncoding }
// request
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
let response = json["response"] as? [String: Any],
let messages = json["messages"] as? [[String: Any]],
let message = messages[0]["message"] as? String,
let code = messages[0]["code"] as? String
else { throw FMError.sessionResponse }
// return
switch code {
case "0":
guard let data = response["data"] as? [[String: Any]],
let records = try? JSONSerialization.data(withJSONObject: data),
let dataInfo = result.response.dataInfo
else { throw FMError.jsonSerialization }
print("fetched \(dataInfo.foundCount) records")
return (records, dataInfo)
default:
print(message)
throw FMError.nonZeroCode
}
}
✨ I'm including a complete SwiftUI example this time, showing the model
, view
, and a fetchArtists(token:)
method. For those unfamiliar with SwiftUI, it's helpful to start in the middle of the example code and work your way out. Here's the gist:
There is a .task
on List
which will return data (async) from FileMaker. I'm using that to set our @State var artists
array. When a @State
property is modified, any view depending on it will be called again. In our case, this recalls body
, refreshing List
with our record data. Neat.
// model
struct Artist: Codable {
let recordId: String // ✨ useful as a \.keyPath in List views
let modId: String
let fieldData: FieldData
struct FieldData: Codable {
let name: String
}
}
// view
struct ContentView: View {
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
// our data source
@State private var artists = [Artist]()
var body: some View {
NavigationView {
List(artists, id: \.recordId) { artist in
Text(artist.fieldData.name) // 🥰 type-safe, Codable properties
}
.navigationTitle("Artists")
.task { // ✅ <-- start here
let isValid = await SwiftFM.validateSession(token: token)
switch isValid {
case true:
await fetchArtists(token: token)
case false:
if let newToken = await SwiftFM.newSession() {
await fetchArtists(token: newToken)
}
}
} // .list
}
}
// ...
// fetch 20 artists
func fetchArtists(token: String) async {
guard let (data, _) = try? await SwiftFM.getRecords(layout: "Artists", limit: 20, sortField: "name", ascending: true, portal: nil, token: token)
let records = try? JSONDecoder().decode([Artist].self, from: data)
else { return }
self.artists = records // sets our @State artists array 👆
}
// ...
}
Returns a record
and dataInfo
response.
func getRecord(id: Int, layout: String, token: String) async throws -> (Data, FMResult.DataInfo) {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
else { throw FMError.urlEncoding }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
let response = json["response"] as? [String: Any],
let messages = json["messages"] as? [[String: Any]],
let message = messages[0]["message"] as? String,
let code = messages[0]["code"] as? String
else { throw FMError.sessionResponse }
// return
switch code {
case "0":
guard let data = response["data"] as? [[String: Any]],
let data0 = data.first,
let record = try? JSONSerialization.data(withJSONObject: data0),
let dataInfo = result.response.dataInfo
else { throw FMError.jsonSerialization }
print("fetched recordId: \(id)")
return (record, dataInfo)
default:
print(message)
throw FMError.nonZeroCode
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"
guard let (data, _) = try? await SwiftFM.getRecord(id: recid, layout: layout, token: token),
let record = try? JSONDecoder().decode(Artist.self, from: data)
else { return }
self.artist = record
FileMaker Data API 18 or later. Returns a Bool
. Make this call with a [String: Any]
object containing a globalFields
key.
func setGlobals(payload: [String: Any], token: String) async -> Bool {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/globals"),
let body = try? JSONSerialization.data(withJSONObject: payload)
else { return false }
var request = URLRequest(url: url)
request.httpMethod = "PATCH"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = body
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.first
else { return false }
// return
switch message.code {
case "0":
print("globals set")
return true
default:
print(message)
return false
}
}
table name::field name
. Also note that our result is a Bool
and doesn't need to be unwrapped.
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let payload = ["globalFields": [
"baseTable::gField": "newValue",
"baseTable::gField2": "newValue"
]]
let result = await SwiftFM.setGlobals(payload: payload, token: token)
if result == true {
print("globals set")
}
FileMaker Data API 18 or later. Returns an optional .productInfo
object.
func getProductInfo() async -> FMProduct.ProductInfo? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/productInfo")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMProduct.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
let info = result.response.productInfo
print("product: \(info.name) (\(info.version))")
return info
default:
print(message)
return nil
}
}
This call doesn't require a token.
guard let info = await SwiftFM.getProductInfo() else { return }
print(info.version) // properties for .name .buildDate, .dateFormat, .timeFormat, and .timeStampFormat
FileMaker Data API 18 or later. Returns an optional array of .database
objects.
func getDatabases() async -> [FMDatabases.Database]? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMDatabases.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
let databases = result.response.databases
print("\(databases.count) databases")
return databases
default:
print(message)
return nil
}
}
This call doesn't require a token.
guard let databases = await SwiftFM.getDatabases() else { return }
print("\nDatabases:")
_ = databases.map{ print($0.name) } // like a .forEach, but shorter
FileMaker Data API 18 or later. Returns an optional array of .layout
objects.
func getLayouts(token: String) async -> [FMLayouts.Layout]? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMLayouts.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
let layouts = result.response.layouts
print("\(layouts.count) layouts")
return layouts
default:
print(message)
return nil
}
}
Many SwiftFM result types conform to Comparable
. 🥰 As such, you can use methods like .sorted()
, min()
, and max()
.
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
guard let layouts = await SwiftFM.getLayouts(token: token) else { return }
// filter and sort folders
let folders = layouts.filter{ $0.isFolder == true }.sorted()
folders.forEach { folder in
print("\n\(folder.name)")
// tab indent folder contents
if let items = folder.folderLayoutNames?.sorted() {
items.forEach { item in
print("\t\(item.name)")
}
}
}
FileMaker Data API 18 or later. Returns an optional .response
object, containing .fields
and .valueList
data. A .portalMetaData
object is included as well, but will be unique to your FileMaker schema. So you'll need to model that yourself.
func getLayoutMetadata(layout: String, token: String) async -> FMLayoutMetaData.Response? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMLayoutMetaData.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
if let fields = result.response.fieldMetaData {
print("\(fields.count) fields")
}
if let valueLists = result.response.valueLists {
print("\(valueLists.count) value lists")
}
return result.response
default:
print(message)
return nil
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"
guard let result = await SwiftFM.getLayoutMetadata(layout: layout, token: token) else { return }
if let fields = result.fieldMetaData?.sorted() {
print("\nFields:")
_ = fields.map { print($0.name) }
}
if let valueLists = result.valueLists?.sorted() {
print("\nValue Lists:")
_ = valueLists.map { print($0.name) }
}
FileMaker Data API 18 or later. Returns an optional array of .script
objects.
func getScripts(token: String) async -> [FMScripts.Script]? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/scripts")
else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMScripts.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
let scripts = result.response.scripts
print("\(scripts.count) scripts")
return scripts
default:
print(message)
return nil
}
}
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
guard let scripts = await SwiftFM.getScripts(token: token) else { return }
// filter and sort folders
let folders = scripts.filter{ $0.isFolder == true }.sorted()
folders.forEach { folder in
print("\n\(folder.name)")
// tab indent folder contents
if let scripts = folder.folderScriptNames?.sorted() {
scripts.forEach { item in
print("\t\(item.name)")
}
}
}
Returns a Bool
.
func executeScript(script: String, parameter: String?, layout: String, token: String) async -> Bool {
// parameter
var param = "" // nil parameter
if let parameter { // non-nil parameter
param = parameter
}
// encoded
guard let scriptEnc = script.urlEncoded, // StringExtension.swift
let paramEnc = param.urlEncoded
else { return false }
// url
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/script/\(scriptEnc)?script.param=\(paramEnc)")
else { return false }
// request
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.first
else { return false }
// return
switch message.code {
case "0":
print("fired script: \(script)")
return true
default:
print(message)
return false
}
}
Script
and parameter
values are .urlEncoded
, so spaces and such are ok.
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let script = "test script"
let layout = "Artists"
let result = await SwiftFM.executeScript(script: script, parameter: nil, layout: layout, token: token)
if result == true {
print("fired script: \(script)")
}
func setContainer(recordId: Int,
layout: String,
container: String,
filePath: URL,
inferType: Bool,
token: String) async -> String? {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(recordId)/containers/\(container)")
else { return nil }
// request
let boundary = UUID().uuidString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// file data
guard let fileData = try? Data(contentsOf: filePath) else { return nil }
let mimeType = inferType ? fileData.mimeType : "application/octet-stream" // DataExtension.swift
// body
let br = "\r\n"
let fileName = filePath.lastPathComponent // ✨ <-- method return
var httpBody = Data()
httpBody.append("\(br)--\(boundary)\(br)")
httpBody.append("Content-Disposition: form-data; name=upload; filename=\(fileName)\(br)")
httpBody.append("Content-Type: \(mimeType)\(br)\(br)")
httpBody.append(fileData)
httpBody.append("\(br)--\(boundary)--\(br)")
request.setValue(String(httpBody.count), forHTTPHeaderField: "Content-Length")
request.httpBody = httpBody
// session
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.first
else { return nil }
// return
switch message.code {
case "0":
print("container set: \(fileName)")
return fileName
default:
print(message)
return nil
}
}
An inferType
of true
will use DataExtension.swift
(extensions folder) to attempt to set the mime-type automatically. If you don't want this behavior, set inferType
to false
, which assigns a default mime-type of "application/octet-stream".
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"
let field = "headshot"
guard let url = URL(string: "http://starsite.co/brian_memoji.png"),
let fileName = await SwiftFM.setContainer(recordId: recid,
layout: layout,
container: field,
filePath: url,
inferType: true,
token: token)
else { return }
print("container set: \(fileName)")
Starsite Labs 😘