项目作者: starsite

项目描述 :
A Swift wrapper for the FileMaker Data API
高级语言: Swift
项目地址: git://github.com/starsite/SwiftFM.git
创建时间: 2018-09-12T04:58:31Z
项目社区:https://github.com/starsite/SwiftFM

开源协议:

下载


alt text

GitHub release (latest SemVer) GitHub

SwiftFM

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.


🗳 How To Use

  • 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!

🖐 How To Help

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. 🥰

✅ Async/await

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.


📔 Table of Contents


Environment Variables

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. 😵

Example: Swift (UIKit)

Set your environment in AppDelegate inside applicationWillEnterForeground(_:).

  1. class AppDelegate: UIResponder, UIApplicationDelegate {
  2. // ...
  3. func applicationWillEnterForeground(_ application: UIApplication) {
  4. let host = "my.host.com" //
  5. let db = "my_database" //
  6. // fetch these from elsewhere or prompt at launch
  7. let user = "username" //
  8. let pass = "password" //
  9. UserDefaults.standard.set(host, forKey: "fm-host")
  10. UserDefaults.standard.set(db, forKey: "fm-db")
  11. let str = "\(user):\(pass)"
  12. if let auth = str.data(using: .utf8)?.base64EncodedString() {
  13. UserDefaults.standard.set(auth, forKey: "fm-auth")
  14. }
  15. }
  16. // ...
  17. }

Example: SwiftUI

Set your environment in MyApp: App. If you don’t see an init() function, add one and finish it out like this.

  1. @main
  2. struct MyApp: App {
  3. init() {
  4. let host = "my.host.com" //
  5. let db = "my_database" //
  6. // fetch these from elsewhere or prompt at launch
  7. let user = "username" //
  8. let pass = "password" //
  9. UserDefaults.standard.set(host, forKey: "fm-host")
  10. UserDefaults.standard.set(db, forKey: "fm-db")
  11. let str = "\(user):\(pass)"
  12. if let auth = str.data(using: .utf8)?.base64EncodedString() {
  13. UserDefaults.standard.set(auth, forKey: "fm-auth")
  14. }
  15. }
  16. var body: some Scene {
  17. // ...
  18. }
  19. }

✨ New Session (function) -> .token?

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.

  1. func newSession() async -> String? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let auth = UserDefaults.standard.string(forKey: "fm-auth"),
  5. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/sessions")
  6. else { return nil }
  7. var request = URLRequest(url: url)
  8. request.httpMethod = "POST"
  9. request.setValue("Basic \(auth)", forHTTPHeaderField: "Authorization")
  10. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  11. guard let (data, _) = try? await URLSession.shared.data(for: request),
  12. let result = try? JSONDecoder().decode(FMSession.self, from: data),
  13. let message = result.messages.first
  14. else { return nil }
  15. // return
  16. switch message.code {
  17. case "0":
  18. guard let token = result.response.token else { return nil }
  19. UserDefaults.standard.set(token, forKey: "fm-token")
  20. print("✨ new token » \(token)")
  21. return token
  22. default:
  23. print(message)
  24. return nil
  25. }
  26. }

Example

  1. if let token = await SwiftFM.newSession() {
  2. print("✨ new token » \(token)")
  3. }

Validate Session (function) -> Bool

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.

  1. func validateSession(token: String) async -> Bool {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let url = URL(string: "https://\(host)/fmi/data/vLatest/validateSession")
  4. else { return false }
  5. var request = URLRequest(url: url)
  6. request.httpMethod = "GET"
  7. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  8. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  9. guard let (data, _) = try? await URLSession.shared.data(for: request),
  10. let result = try? JSONDecoder().decode(FMSession.self, from: data),
  11. let message = result.messages.first
  12. else { return false }
  13. // return
  14. switch message.code {
  15. case "0":
  16. print("✅ valid token » \(token)")
  17. return true
  18. default:
  19. print(message)
  20. return false
  21. }
  22. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let isValid = await SwiftFM.validateSession(token: token)
  3. switch isValid {
  4. case true:
  5. fetchArtists(token: token)
  6. case false:
  7. if let newToken = await SwiftFM.newSession() {
  8. fetchArtists(token: newToken)
  9. }
  10. }

Delete Session (function) -> @escaping Bool

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. 🙂

  1. func deleteSession(token: String, completion: @escaping (Bool) -> Void) {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/sessions/\(token)")
  5. else { return }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "DELETE"
  8. URLSession.shared.dataTask(with: request) { data, resp, error in
  9. guard let data = data, error == nil,
  10. let result = try? JSONDecoder().decode(FMSession.self, from: data),
  11. let message = result.messages.first
  12. else { return }
  13. // return
  14. switch message.code {
  15. case "0":
  16. UserDefaults.standard.set(nil, forKey: "fm-token")
  17. print("🔥 deleted token » \(token)")
  18. completion(true)
  19. default:
  20. print(message)
  21. completion(false)
  22. }
  23. }.resume()
  24. }

Example: Swift (UIKit)

  1. @UIApplicationMain
  2. class AppDelegate: UIResponder, UIApplicationDelegate {
  3. // ...
  4. func applicationDidEnterBackground(_ application: UIApplication) {
  5. if let token = UserDefaults.standard.string(forKey: "fm-token") {
  6. SwiftFM.deleteSession(token: token) { _ in }
  7. }
  8. }
  9. // ...
  10. }

Example: SwiftUI

  1. @main
  2. struct MyApp: App {
  3. var body: some Scene {
  4. WindowGroup {
  5. ContentView()
  6. }
  7. .onChange(of: scenePhase) { phase in
  8. switch phase {
  9. case .background:
  10. DispatchQueue.global(qos: .background).async { // extra time
  11. if let token = UserDefaults.standard.string(forKey: "fm-token") {
  12. SwiftFM.deleteSession(token: token) { _ in }
  13. }
  14. }
  15. default: break
  16. }
  17. }
  18. } // .body
  19. }

✨ Create Record (function) -> .recordId?

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.

  1. func createRecord(layout: String, payload: [String: Any]?, token: String) async -> String? {
  2. var fieldData: [String: Any] = ["fieldData": [:]] // nil payload
  3. if let payload { // non-nil payload
  4. fieldData = payload
  5. }
  6. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  7. let db = UserDefaults.standard.string(forKey: "fm-db"),
  8. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records"),
  9. let body = try? JSONSerialization.data(withJSONObject: fieldData)
  10. else { return nil }
  11. var request = URLRequest(url: url)
  12. request.httpMethod = "POST"
  13. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  14. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  15. request.httpBody = body
  16. guard let (data, _) = try? await URLSession.shared.data(for: request),
  17. let result = try? JSONDecoder().decode(FMRecord.self, from: data),
  18. let message = result.messages.first
  19. else { return nil }
  20. // return
  21. switch message.code {
  22. case "0":
  23. guard let recordId = result.response.recordId else { return nil }
  24. print("✨ new recordId: \(recordId)")
  25. return recordId
  26. default:
  27. print(message)
  28. return nil
  29. }
  30. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let layout = "Artists"
  3. let payload = ["fieldData": [ // required key
  4. "firstName": "Brian",
  5. "lastName": "Hamm",
  6. "email": "hello@starsite.co"
  7. ]]
  8. if let recordId = await SwiftFM.createRecord(layout: layout, payload: payload, token: token) {
  9. print("created record: \(recordId)")
  10. }

Duplicate Record (function) -> .recordId?

FileMaker Data API 18 or later. Pretty simple call. Returns an optional recordId for the new record.

  1. func duplicateRecord(id: Int, layout: String, token: String) async -> String? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
  5. else { return nil }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "POST"
  8. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  9. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  10. guard let (data, _) = try? await URLSession.shared.data(for: request),
  11. let result = try? JSONDecoder().decode(FMRecord.self, from: data),
  12. let message = result.messages.first
  13. else { return nil }
  14. // return
  15. switch message.code {
  16. case "0":
  17. guard let recordId = result.response.recordId else { return nil }
  18. print("✨ new recordId: \(recordId)")
  19. return recordId
  20. default:
  21. print(message)
  22. return nil
  23. }
  24. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let recid = 12345
  3. let layout = "Artists"
  4. if let recordId = await SwiftFM.duplicateRecord(id: recid, layout: layout, token: token) {
  5. print("new record: \(recordId)")
  6. }

Edit Record (function) -> .modId?

Returns an optional modId. Pass a [String: Any] object with a fieldData key containing the fields you want to modify.

⚠️ If you include the 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:).

  1. func editRecord(id: Int, layout: String, payload: [String: Any], token: String) async -> String? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)"),
  5. let body = try? JSONSerialization.data(withJSONObject: payload)
  6. else { return nil }
  7. var request = URLRequest(url: url)
  8. request.httpMethod = "PATCH"
  9. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  10. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  11. request.httpBody = body
  12. guard let (data, _) = try? await URLSession.shared.data(for: request),
  13. let result = try? JSONDecoder().decode(FMRecord.self, from: data),
  14. let message = result.messages.first
  15. else { return nil }
  16. // return
  17. switch message.code {
  18. case "0":
  19. guard let modId = result.response.modId else { return nil }
  20. print("updated modId: \(modId)")
  21. return modId
  22. default:
  23. print(message)
  24. return nil
  25. }
  26. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let recid = 12345
  3. let layout = "Artists"
  4. let payload = ["fieldData": [
  5. "address": "My updated address",
  6. ]]
  7. if let modId = await SwiftFM.editRecord(id: recid, layout: layout, payload: payload, token: token) {
  8. print("updated modId: \(modId)")
  9. }

🔥 Delete Record (function) -> Bool

Pretty self explanatory. Returns a Bool.

  1. func deleteRecord(id: Int, layout: String, token: String) async -> Bool {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
  5. else { return false }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "DELETE"
  8. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  9. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  10. guard let (data, _) = try? await URLSession.shared.data(for: request),
  11. let result = try? JSONDecoder().decode(FMBool.self, from: data),
  12. let message = result.messages.first
  13. else { return false }
  14. // return
  15. switch message.code {
  16. case "0":
  17. print("deleted recordId: \(id)")
  18. return true
  19. default:
  20. print(message)
  21. return false
  22. }
  23. }

Example

⚠️ This is Swift, not FileMaker. Nothing will prevent this from firing—immediately. Put some kind of confirmation view in your app.

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let recid = 12345
  3. let layout = "Artists"
  4. let result = await SwiftFM.deleteRecord(id: recid, layout: layout, token: token)
  5. if result == true {
  6. print("deleted recordId \(recordId)")
  7. }

🔍 Query (function) -> ([record], .dataInfo)

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.

  1. func query(layout: String, payload: [String: Any], token: String) async throws -> (Data, FMResult.DataInfo) {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/_find"),
  5. let body = try? JSONSerialization.data(withJSONObject: payload)
  6. else { throw FMError.jsonSerialization }
  7. var request = URLRequest(url: url)
  8. request.httpMethod = "POST"
  9. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  10. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  11. request.httpBody = body
  12. guard let (data, _) = try? await URLSession.shared.data(for: request),
  13. let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
  14. let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
  15. let response = json["response"] as? [String: Any],
  16. let messages = json["messages"] as? [[String: Any]],
  17. let message = messages[0]["message"] as? String,
  18. let code = messages[0]["code"] as? String
  19. else { throw FMError.sessionResponse }
  20. // return
  21. switch code {
  22. case "0":
  23. guard let data = response["data"] as? [[String: Any]],
  24. let records = try? JSONSerialization.data(withJSONObject: data),
  25. let dataInfo = result.response.dataInfo
  26. else { throw FMError.jsonSerialization }
  27. print("fetched \(dataInfo.foundCount) records")
  28. return (records, dataInfo)
  29. default:
  30. print(message)
  31. throw FMError.nonZeroCode
  32. }
  33. }

Example

Note the difference in payload between an “or” request vs. an “and” request.

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let layout = "Artists"
  3. // find artists named Brian or Geoff
  4. let payload = ["query": [
  5. ["firstName": "Brian"],
  6. ["firstName": "Geoff"]
  7. ]]
  8. // find artists named Brian in Dallas
  9. let payload = ["query": [
  10. ["firstName": "Brian", "city": "Dallas"]
  11. ]]
  12. guard let (data, _) = try? await SwiftFM.query(layout: layout, payload: payload, token: token),
  13. let records = try? JSONDecoder().decode([Artist].self, from: data)
  14. else { return }
  15. self.artists = records // set @State data source

Get Records (function) -> ([record], .dataInfo)

Returns a record array and dataInfo response. All SwiftFM record fetching methods return a tuple.

  1. func getRecords(layout: String,
  2. limit: Int,
  3. sortField: String,
  4. ascending: Bool,
  5. portal: String?,
  6. token: String) async throws -> (Data, FMResult.DataInfo) {
  7. // param str
  8. let order = ascending ? "ascend" : "descend"
  9. let sortJson = """
  10. [{"fieldName":"\(sortField)","sortOrder":"\(order)"}]
  11. """
  12. var portalJson = "[]" // nil portal
  13. if let portal { // non-nil portal
  14. portalJson = """
  15. ["\(portal)"]
  16. """
  17. }
  18. // encoding
  19. guard let sortEnc = sortJson.urlEncoded,
  20. let portalEnc = portalJson.urlEncoded,
  21. let host = UserDefaults.standard.string(forKey: "fm-host"),
  22. let db = UserDefaults.standard.string(forKey: "fm-db"),
  23. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/?_limit=\(limit)&_sort=\(sortEnc)&portal=\(portalEnc)")
  24. else { throw FMError.urlEncoding }
  25. // request
  26. var request = URLRequest(url: url)
  27. request.httpMethod = "GET"
  28. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  29. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  30. guard let (data, _) = try? await URLSession.shared.data(for: request),
  31. let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
  32. let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
  33. let response = json["response"] as? [String: Any],
  34. let messages = json["messages"] as? [[String: Any]],
  35. let message = messages[0]["message"] as? String,
  36. let code = messages[0]["code"] as? String
  37. else { throw FMError.sessionResponse }
  38. // return
  39. switch code {
  40. case "0":
  41. guard let data = response["data"] as? [[String: Any]],
  42. let records = try? JSONSerialization.data(withJSONObject: data),
  43. let dataInfo = result.response.dataInfo
  44. else { throw FMError.jsonSerialization }
  45. print("fetched \(dataInfo.foundCount) records")
  46. return (records, dataInfo)
  47. default:
  48. print(message)
  49. throw FMError.nonZeroCode
  50. }
  51. }

Example (SwiftUI)

✨ 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.

  1. // model
  2. struct Artist: Codable {
  3. let recordId: String // ✨ useful as a \.keyPath in List views
  4. let modId: String
  5. let fieldData: FieldData
  6. struct FieldData: Codable {
  7. let name: String
  8. }
  9. }
  10. // view
  11. struct ContentView: View {
  12. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  13. // our data source
  14. @State private var artists = [Artist]()
  15. var body: some View {
  16. NavigationView {
  17. List(artists, id: \.recordId) { artist in
  18. Text(artist.fieldData.name) // 🥰 type-safe, Codable properties
  19. }
  20. .navigationTitle("Artists")
  21. .task { // ✅ <-- start here
  22. let isValid = await SwiftFM.validateSession(token: token)
  23. switch isValid {
  24. case true:
  25. await fetchArtists(token: token)
  26. case false:
  27. if let newToken = await SwiftFM.newSession() {
  28. await fetchArtists(token: newToken)
  29. }
  30. }
  31. } // .list
  32. }
  33. }
  34. // ...
  35. // fetch 20 artists
  36. func fetchArtists(token: String) async {
  37. guard let (data, _) = try? await SwiftFM.getRecords(layout: "Artists", limit: 20, sortField: "name", ascending: true, portal: nil, token: token)
  38. let records = try? JSONDecoder().decode([Artist].self, from: data)
  39. else { return }
  40. self.artists = records // sets our @State artists array 👆
  41. }
  42. // ...
  43. }

Get Record (function) -> (record, .dataInfo)

Returns a record and dataInfo response.

  1. func getRecord(id: Int, layout: String, token: String) async throws -> (Data, FMResult.DataInfo) {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
  5. else { throw FMError.urlEncoding }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "GET"
  8. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  9. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  10. guard let (data, _) = try? await URLSession.shared.data(for: request),
  11. let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
  12. let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
  13. let response = json["response"] as? [String: Any],
  14. let messages = json["messages"] as? [[String: Any]],
  15. let message = messages[0]["message"] as? String,
  16. let code = messages[0]["code"] as? String
  17. else { throw FMError.sessionResponse }
  18. // return
  19. switch code {
  20. case "0":
  21. guard let data = response["data"] as? [[String: Any]],
  22. let data0 = data.first,
  23. let record = try? JSONSerialization.data(withJSONObject: data0),
  24. let dataInfo = result.response.dataInfo
  25. else { throw FMError.jsonSerialization }
  26. print("fetched recordId: \(id)")
  27. return (record, dataInfo)
  28. default:
  29. print(message)
  30. throw FMError.nonZeroCode
  31. }
  32. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let recid = 12345
  3. let layout = "Artists"
  4. guard let (data, _) = try? await SwiftFM.getRecord(id: recid, layout: layout, token: token),
  5. let record = try? JSONDecoder().decode(Artist.self, from: data)
  6. else { return }
  7. self.artist = record

Set Globals (function) -> Bool

FileMaker Data API 18 or later. Returns a Bool. Make this call with a [String: Any] object containing a globalFields key.

  1. func setGlobals(payload: [String: Any], token: String) async -> Bool {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/globals"),
  5. let body = try? JSONSerialization.data(withJSONObject: payload)
  6. else { return false }
  7. var request = URLRequest(url: url)
  8. request.httpMethod = "PATCH"
  9. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  10. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  11. request.httpBody = body
  12. guard let (data, _) = try? await URLSession.shared.data(for: request),
  13. let result = try? JSONDecoder().decode(FMBool.self, from: data),
  14. let message = result.messages.first
  15. else { return false }
  16. // return
  17. switch message.code {
  18. case "0":
  19. print("globals set")
  20. return true
  21. default:
  22. print(message)
  23. return false
  24. }
  25. }

Example

⚠️ Global fields must be set using fully qualified field names, ie. table name::field name. Also note that our result is a Bool and doesn’t need to be unwrapped.

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let payload = ["globalFields": [
  3. "baseTable::gField": "newValue",
  4. "baseTable::gField2": "newValue"
  5. ]]
  6. let result = await SwiftFM.setGlobals(payload: payload, token: token)
  7. if result == true {
  8. print("globals set")
  9. }

Get Product Info (function) -> .productInfo?

FileMaker Data API 18 or later. Returns an optional .productInfo object.

  1. func getProductInfo() async -> FMProduct.ProductInfo? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let url = URL(string: "https://\(host)/fmi/data/vLatest/productInfo")
  4. else { return nil }
  5. var request = URLRequest(url: url)
  6. request.httpMethod = "GET"
  7. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  8. guard let (data, _) = try? await URLSession.shared.data(for: request),
  9. let result = try? JSONDecoder().decode(FMProduct.self, from: data),
  10. let message = result.messages.first
  11. else { return nil }
  12. // return
  13. switch message.code {
  14. case "0":
  15. let info = result.response.productInfo
  16. print("product: \(info.name) (\(info.version))")
  17. return info
  18. default:
  19. print(message)
  20. return nil
  21. }
  22. }

Example

This call doesn’t require a token.

  1. guard let info = await SwiftFM.getProductInfo() else { return }
  2. print(info.version) // properties for .name .buildDate, .dateFormat, .timeFormat, and .timeStampFormat

Get Databases (function) -> .databases?

FileMaker Data API 18 or later. Returns an optional array of .database objects.

  1. func getDatabases() async -> [FMDatabases.Database]? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases")
  4. else { return nil }
  5. var request = URLRequest(url: url)
  6. request.httpMethod = "GET"
  7. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  8. guard let (data, _) = try? await URLSession.shared.data(for: request),
  9. let result = try? JSONDecoder().decode(FMDatabases.self, from: data),
  10. let message = result.messages.first
  11. else { return nil }
  12. // return
  13. switch message.code {
  14. case "0":
  15. let databases = result.response.databases
  16. print("\(databases.count) databases")
  17. return databases
  18. default:
  19. print(message)
  20. return nil
  21. }
  22. }

Example

This call doesn’t require a token.

  1. guard let databases = await SwiftFM.getDatabases() else { return }
  2. print("\nDatabases:")
  3. _ = databases.map{ print($0.name) } // like a .forEach, but shorter

Get Layouts (function) -> .layouts?

FileMaker Data API 18 or later. Returns an optional array of .layout objects.

  1. func getLayouts(token: String) async -> [FMLayouts.Layout]? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts")
  5. else { return nil }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "GET"
  8. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  9. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  10. guard let (data, _) = try? await URLSession.shared.data(for: request),
  11. let result = try? JSONDecoder().decode(FMLayouts.self, from: data),
  12. let message = result.messages.first
  13. else { return nil }
  14. // return
  15. switch message.code {
  16. case "0":
  17. let layouts = result.response.layouts
  18. print("\(layouts.count) layouts")
  19. return layouts
  20. default:
  21. print(message)
  22. return nil
  23. }
  24. }

Example

Many SwiftFM result types conform to Comparable. 🥰 As such, you can use methods like .sorted(), min(), and max().

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. guard let layouts = await SwiftFM.getLayouts(token: token) else { return }
  3. // filter and sort folders
  4. let folders = layouts.filter{ $0.isFolder == true }.sorted()
  5. folders.forEach { folder in
  6. print("\n\(folder.name)")
  7. // tab indent folder contents
  8. if let items = folder.folderLayoutNames?.sorted() {
  9. items.forEach { item in
  10. print("\t\(item.name)")
  11. }
  12. }
  13. }

Get Layout Metadata (function) -> .response?

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.

  1. func getLayoutMetadata(layout: String, token: String) async -> FMLayoutMetaData.Response? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)")
  5. else { return nil }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "GET"
  8. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  9. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  10. guard let (data, _) = try? await URLSession.shared.data(for: request),
  11. let result = try? JSONDecoder().decode(FMLayoutMetaData.self, from: data),
  12. let message = result.messages.first
  13. else { return nil }
  14. // return
  15. switch message.code {
  16. case "0":
  17. if let fields = result.response.fieldMetaData {
  18. print("\(fields.count) fields")
  19. }
  20. if let valueLists = result.response.valueLists {
  21. print("\(valueLists.count) value lists")
  22. }
  23. return result.response
  24. default:
  25. print(message)
  26. return nil
  27. }
  28. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let layout = "Artists"
  3. guard let result = await SwiftFM.getLayoutMetadata(layout: layout, token: token) else { return }
  4. if let fields = result.fieldMetaData?.sorted() {
  5. print("\nFields:")
  6. _ = fields.map { print($0.name) }
  7. }
  8. if let valueLists = result.valueLists?.sorted() {
  9. print("\nValue Lists:")
  10. _ = valueLists.map { print($0.name) }
  11. }

Get Scripts (function) -> .scripts?

FileMaker Data API 18 or later. Returns an optional array of .script objects.

  1. func getScripts(token: String) async -> [FMScripts.Script]? {
  2. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  3. let db = UserDefaults.standard.string(forKey: "fm-db"),
  4. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/scripts")
  5. else { return nil }
  6. var request = URLRequest(url: url)
  7. request.httpMethod = "GET"
  8. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  9. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  10. guard let (data, _) = try? await URLSession.shared.data(for: request),
  11. let result = try? JSONDecoder().decode(FMScripts.self, from: data),
  12. let message = result.messages.first
  13. else { return nil }
  14. // return
  15. switch message.code {
  16. case "0":
  17. let scripts = result.response.scripts
  18. print("\(scripts.count) scripts")
  19. return scripts
  20. default:
  21. print(message)
  22. return nil
  23. }
  24. }

Example

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. guard let scripts = await SwiftFM.getScripts(token: token) else { return }
  3. // filter and sort folders
  4. let folders = scripts.filter{ $0.isFolder == true }.sorted()
  5. folders.forEach { folder in
  6. print("\n\(folder.name)")
  7. // tab indent folder contents
  8. if let scripts = folder.folderScriptNames?.sorted() {
  9. scripts.forEach { item in
  10. print("\t\(item.name)")
  11. }
  12. }
  13. }

Execute Script (function) -> Bool

Returns a Bool.

  1. func executeScript(script: String, parameter: String?, layout: String, token: String) async -> Bool {
  2. // parameter
  3. var param = "" // nil parameter
  4. if let parameter { // non-nil parameter
  5. param = parameter
  6. }
  7. // encoded
  8. guard let scriptEnc = script.urlEncoded, // StringExtension.swift
  9. let paramEnc = param.urlEncoded
  10. else { return false }
  11. // url
  12. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  13. let db = UserDefaults.standard.string(forKey: "fm-db"),
  14. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/script/\(scriptEnc)?script.param=\(paramEnc)")
  15. else { return false }
  16. // request
  17. var request = URLRequest(url: url)
  18. request.httpMethod = "GET"
  19. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  20. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  21. guard let (data, _) = try? await URLSession.shared.data(for: request),
  22. let result = try? JSONDecoder().decode(FMBool.self, from: data),
  23. let message = result.messages.first
  24. else { return false }
  25. // return
  26. switch message.code {
  27. case "0":
  28. print("fired script: \(script)")
  29. return true
  30. default:
  31. print(message)
  32. return false
  33. }
  34. }

Example

Script and parameter values are .urlEncoded, so spaces and such are ok.

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let script = "test script"
  3. let layout = "Artists"
  4. let result = await SwiftFM.executeScript(script: script, parameter: nil, layout: layout, token: token)
  5. if result == true {
  6. print("fired script: \(script)")
  7. }

Set Container (function) -> fileName?

  1. func setContainer(recordId: Int,
  2. layout: String,
  3. container: String,
  4. filePath: URL,
  5. inferType: Bool,
  6. token: String) async -> String? {
  7. guard let host = UserDefaults.standard.string(forKey: "fm-host"),
  8. let db = UserDefaults.standard.string(forKey: "fm-db"),
  9. let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(recordId)/containers/\(container)")
  10. else { return nil }
  11. // request
  12. let boundary = UUID().uuidString
  13. var request = URLRequest(url: url)
  14. request.httpMethod = "POST"
  15. request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  16. request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
  17. // file data
  18. guard let fileData = try? Data(contentsOf: filePath) else { return nil }
  19. let mimeType = inferType ? fileData.mimeType : "application/octet-stream" // DataExtension.swift
  20. // body
  21. let br = "\r\n"
  22. let fileName = filePath.lastPathComponent // ✨ <-- method return
  23. var httpBody = Data()
  24. httpBody.append("\(br)--\(boundary)\(br)")
  25. httpBody.append("Content-Disposition: form-data; name=upload; filename=\(fileName)\(br)")
  26. httpBody.append("Content-Type: \(mimeType)\(br)\(br)")
  27. httpBody.append(fileData)
  28. httpBody.append("\(br)--\(boundary)--\(br)")
  29. request.setValue(String(httpBody.count), forHTTPHeaderField: "Content-Length")
  30. request.httpBody = httpBody
  31. // session
  32. guard let (data, _) = try? await URLSession.shared.data(for: request),
  33. let result = try? JSONDecoder().decode(FMBool.self, from: data),
  34. let message = result.messages.first
  35. else { return nil }
  36. // return
  37. switch message.code {
  38. case "0":
  39. print("container set: \(fileName)")
  40. return fileName
  41. default:
  42. print(message)
  43. return nil
  44. }
  45. }

Example

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”.

  1. let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
  2. let recid = 12345
  3. let layout = "Artists"
  4. let field = "headshot"
  5. guard let url = URL(string: "http://starsite.co/brian_memoji.png"),
  6. let fileName = await SwiftFM.setContainer(recordId: recid,
  7. layout: layout,
  8. container: field,
  9. filePath: url,
  10. inferType: true,
  11. token: token)
  12. else { return }
  13. print("container set: \(fileName)")

Starsite Labs 😘