SwiftFM 是一个用于 FileMaker Data API 的 Swift 包。它使用现代 Swift 特性,例如 async/await
、Codable
类型安全返回,并对 DocC
提供广泛支持。
此 README.md
旨在帮助希望在 UIKit 和 SwiftUI 项目中使用 Data API 的 Swift 开发者。下面显示的每个函数都配有代码示例。
SwiftFM 与 FileMaker iOS App SDK 没有任何关系。
https://github.com/starsite/SwiftFM.git
applicationWillEnterForeground(_:)
中设置您的环境MyApp.init()
中设置您的环境import SwiftFM
语句SwiftFM.newSession()
并获取令牌 ✨如果您想支持 SwiftFM 项目,您可以
SwiftFM 去年被重写以使用 async/await
。这需要 Swift 5.5 和 iOS 15。如果您需要为 iOS 13 或 14 编译,请跳过 SPM 并改为下载 repo,并使用 withCheckedContinuation
转换 URLSession
调用。有关这方面的更多信息,请访问:Swift by Sundell、Hacking With Swift,或观看 Apple 的 WWDC 2021 关于此主题的会议。
环境变量
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:)
对于测试,您可以使用字符串字面量设置这些变量。对于生产环境,您应该从其他地方获取这些值。请勿部署在代码中可见凭据的应用程序。😵
在 AppDelegate
的 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")
}
}
// ...
}
在 MyApp: App
中设置您的环境。如果您没有看到 init()
函数,请添加一个并像这样完成它。
@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 {
// ...
}
}
返回一个可选的 token
。
如果由于不正确的 Authorization
导致此操作失败,FileMaker Data API 将向控制台返回错误 code
和 message
。所有 SwiftFM 调用都会输出一个简单的成功或失败消息。
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 或更高版本。返回 Bool
。此函数本身并不是非常有用。但是您可以使用它来包装其他调用,以确保它们使用有效的 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)
}
}
返回 Bool
。对于标准 Swift (UIKit) 应用程序,调用此函数的理想位置是 applicationDidEnterBackground(_:)
。对于 SwiftUI 应用程序,您应该在 \.scenePhase.background
开关内调用它。
FileMaker 的 Data API 有 500 个会话的限制,因此管理会话令牌对于大型部署非常重要。如果您不删除会话令牌,它将应该在上次 API 调用后 15 分钟过期。可能吧。但是您应该自己清理,而不要假设会发生这种情况。🙂
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
}
返回一个可选的 recordId
。可以在有或没有 payload 的情况下调用此函数。如果您设置 nil
payload,将创建一个新的空记录。两种方法都将返回一个 recordId
。使用包含 fieldData
键的 [String: Any]
对象设置您的 payload。
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 或更高版本。非常简单的调用。为新记录返回一个可选的 recordId
。
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)")
}
返回一个可选的 modId
。传递一个带有 fieldData
键的 [String: Any]
对象,其中包含您要修改的字段。
payload
中包含 modId
值(例如,从先前的 fetch 中获得),则仅当 modId
与 FileMaker Server 上的值匹配时,记录才会被修改。这确保您正在使用记录的当前版本。如果您不传递 modId
,您的更改将在没有此检查的情况下应用。
注意:FileMaker Data API 不会传回修改后的记录对象供您使用。因此,您可能需要在之后使用 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)")
}
非常不言自明。返回 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)")
}
返回一个 record
数组和 dataInfo
响应。这是我们的第一个返回元组的函数。您可以使用任一对象(或两者)。dataInfo
对象包括有关请求的元数据(数据库、布局和表;以及总数、找到和返回的记录计数)。如果您想忽略 dataInfo
,您可以将其分配给下划线。
您可以从 UI 设置您的 payload
,或硬编码查询。然后将其作为带有 query
键的 [String: Any]
对象传递。
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
}
}
请注意“或”请求与“与”请求之间 payload 的差异。
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
返回一个 record
数组和 dataInfo
响应。所有 SwiftFM 记录获取方法都返回一个元组。
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
}
}
✨ 这次我包含了一个完整的 SwiftUI 示例,展示了 model
、view
和 fetchArtists(token:)
方法。对于那些不熟悉 SwiftUI 的人,从示例代码的中间开始,然后向外展开会很有帮助。这是要点
List
上有一个 .task
,它将从 FileMaker 返回数据(异步)。我正在使用它来设置我们的 @State var artists
数组。当 @State
属性被修改时,任何依赖于它的视图都将再次被调用。在我们的例子中,这将重新调用 body
,使用我们的记录数据刷新 List
。真棒。
// 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 👆
}
// ...
}
返回一个 record
和 dataInfo
响应。
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 或更高版本。返回 Bool
。使用包含 globalFields
键的 [String: Any]
对象进行此调用。
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
。另请注意,我们的结果是 Bool
,不需要解包。
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 或更高版本。返回一个可选的 .productInfo
对象。
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
}
}
此调用不需要令牌。
guard let info = await SwiftFM.getProductInfo() else { return }
print(info.version) // properties for .name .buildDate, .dateFormat, .timeFormat, and .timeStampFormat
FileMaker Data API 18 或更高版本。返回 .database
对象的可选数组。
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
}
}
此调用不需要令牌。
guard let databases = await SwiftFM.getDatabases() else { return }
print("\nDatabases:")
_ = databases.map{ print($0.name) } // like a .forEach, but shorter
FileMaker Data API 18 或更高版本。返回 .layout
对象的可选数组。
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
}
}
许多 SwiftFM 结果类型都符合 Comparable
。🥰 因此,您可以使用 .sorted()
、min()
和 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 或更高版本。返回一个可选的 .response
对象,其中包含 .fields
和 .valueList
数据。还包含一个 .portalMetaData
对象,但它对于您的 FileMaker 架构是唯一的。因此,您需要自己建模。
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 或更高版本。返回 .script
对象的可选数组。
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)")
}
}
}
返回 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
和 parameter
值是 .urlEncoded
的,因此空格等是可以的。
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
}
}
inferType
为 true
将使用 DataExtension.swift
(extensions 文件夹)尝试自动设置 mime 类型。如果您不想要此行为,请将 inferType
设置为 false
,这将分配默认的 mime 类型“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 😘