From b71b4a38ebfa066872435e44c09ff618da840ebe Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 12 Nov 2023 12:47:48 -0800 Subject: [PATCH] Update to Spezi 0.8.0 (#37) # Update to Spezi 0.8.0 ## :gear: Release Notes - Small maintenance PR to update the Package to Spezi 0.8.0 ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --- Package.swift | 8 +- README.md | 29 +++-- Sources/SpeziOpenAI/MessageInputView.swift | 2 +- Sources/SpeziOpenAI/MessagesView.swift | 2 +- .../OpenAIAPIKeyOnboardingStep.swift | 4 +- Sources/SpeziOpenAI/OpenAIComponent.swift | 94 ---------------- Sources/SpeziOpenAI/OpenAIModel.swift | 101 ++++++++++++++++++ .../OpenAIModelSelectionOnboardingStep.swift | 5 +- Sources/SpeziOpenAI/OpenAIModule.swift | 43 ++++++++ .../SpeziOpenAI.docc/SpeziOpenAI.md | 39 ++++--- Tests/UITests/TestApp/TestAppDelegate.swift | 2 +- .../UITests/UITests.xcodeproj/project.pbxproj | 10 +- 12 files changed, 205 insertions(+), 134 deletions(-) delete mode 100644 Sources/SpeziOpenAI/OpenAIComponent.swift create mode 100644 Sources/SpeziOpenAI/OpenAIModel.swift create mode 100644 Sources/SpeziOpenAI/OpenAIModule.swift diff --git a/Package.swift b/Package.swift index 1b19851..3ecb867 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( name: "SpeziML", defaultLocalization: "en", platforms: [ - .iOS(.v16) + .iOS(.v17) ], products: [ .library(name: "SpeziOpenAI", targets: ["SpeziOpenAI"]), @@ -24,9 +24,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/MacPaw/OpenAI", .upToNextMinor(from: "0.2.4")), - .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0")), - .package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.4.0")), - .package(url: "https://github.com/StanfordSpezi/SpeziOnboarding", .upToNextMinor(from: "0.6.0")) + .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")), + .package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.5.0")), + .package(url: "https://github.com/StanfordSpezi/SpeziOnboarding", .upToNextMinor(from: "0.7.0")) ], targets: [ .target( diff --git a/README.md b/README.md index 59880de..1e833d4 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,13 @@ First, you will need to add the SpeziML Swift package to [your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or [Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). When adding the package, select the `SpeziOpenAI` target to add. -### 2. Register the Open AI Component +### 2. Register the Open AI Module > [!IMPORTANT] > If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure. -You can configure the `OpenAIComponent` in the `SpeziAppDelegate` as follows. +You can configure the `OpenAIModule` in the `SpeziAppDelegate` as follows. +In the example, we configure the `OpenAIModule` to use the GPT-4 model with a default API key. ```swift import Spezi @@ -53,18 +54,32 @@ import SpeziOpenAI class ExampleDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - OpenAIComponent(apiToken: "API_KEY", openAIModel: .gpt4) + OpenAIModule(apiToken: "API_KEY", openAIModel: .gpt4) } } } ``` -In the example above, we have configured the `OpenAIComponent` to use the GPT-4 model with a default API key. Note that the choice of model and API key are persisted across application launches. The `apiToken` and `openAIModel` can also be accessed and changed at runtime. +The OpenAIModule injects an ``OpenAIModel`` in the SwiftUI environment to make it accessible thoughout your application. + +```swift +class ExampleView: View { + @Environment(OpenAIModel.self) var model + + + var body: some View { + // ... + } +} +``` + +> [!NOTE] +> The choice of model and API key are persisted across application launches. The `apiToken` and `openAIModel` can also be accessed and changed at runtime. The `SpeziOpenAI` package also provides an `OpenAIAPIKeyOnboardingStep` that can be used to allow the user to provide their API key during the onboarding process instead (see `Examples` below). If using the `OpenAIAPIKeyOnboardingStep`, the `apiToken` property can be omitted here. > [!NOTE] -> You can learn more about a [`Component` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component). +> You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). ## Examples @@ -78,13 +93,13 @@ import SpeziOpenAI import SwiftUI struct OpenAIChatView: View { - @EnvironmentObject private var openAIComponent: OpenAIComponent + @Environment(OpenAIModel.self) var model @State private var chat: [Chat] var body: some View { ChatView($chat) .onChange(of: chat) { _ in - let chatStreamResults = try await openAIComponent.queryAPI(withChat: chat) + let chatStreamResults = try await model.queryAPI(withChat: chat) for try await chatStreamResult in chatStreamResults { for choice in chatStreamResult.choices { diff --git a/Sources/SpeziOpenAI/MessageInputView.swift b/Sources/SpeziOpenAI/MessageInputView.swift index aa8c266..4464aff 100644 --- a/Sources/SpeziOpenAI/MessageInputView.swift +++ b/Sources/SpeziOpenAI/MessageInputView.swift @@ -59,7 +59,7 @@ public struct MessageInputView: View { .onAppear { messageViewHeight = proxy.size.height } - .onChange(of: message) { _ in + .onChange(of: message) { messageViewHeight = proxy.size.height } } diff --git a/Sources/SpeziOpenAI/MessagesView.swift b/Sources/SpeziOpenAI/MessagesView.swift index ea74e74..9a78d1b 100644 --- a/Sources/SpeziOpenAI/MessagesView.swift +++ b/Sources/SpeziOpenAI/MessagesView.swift @@ -52,7 +52,7 @@ public struct MessagesView: View { .onAppear { scrollToBottom(scrollViewProxy) } - .onChange(of: chat) { _ in + .onChange(of: chat) { scrollToBottom(scrollViewProxy) } .onReceive(keyboardPublisher) { _ in diff --git a/Sources/SpeziOpenAI/OpenAIAPIKeyOnboardingStep.swift b/Sources/SpeziOpenAI/OpenAIAPIKeyOnboardingStep.swift index a98f60e..bb23e77 100644 --- a/Sources/SpeziOpenAI/OpenAIAPIKeyOnboardingStep.swift +++ b/Sources/SpeziOpenAI/OpenAIAPIKeyOnboardingStep.swift @@ -14,7 +14,7 @@ import SwiftUI /// View to display an onboarding step for the user to enter an OpenAI API Key. public struct OpenAIAPIKeyOnboardingStep: View { - @EnvironmentObject private var openAI: OpenAIComponent + @Environment(OpenAIModel.self) private var openAI private let actionText: String private let action: () -> Void @@ -64,7 +64,7 @@ public struct OpenAIAPIKeyOnboardingStep: View { }, actionView: { OnboardingActionsView( - actionText, + verbatim: actionText, action: { action() } diff --git a/Sources/SpeziOpenAI/OpenAIComponent.swift b/Sources/SpeziOpenAI/OpenAIComponent.swift deleted file mode 100644 index 907f2e4..0000000 --- a/Sources/SpeziOpenAI/OpenAIComponent.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// This source file is part of the Stanford Spezi open source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -import OpenAI -@_exported import struct OpenAI.Model -@_exported import struct OpenAI.ChatStreamResult -import Spezi -import SpeziLocalStorage -import SpeziSecureStorage -import SwiftUI - - -/// `OpenAIComponent` is a module responsible for to coordinate the interactions with the OpenAI GPT API. -public class OpenAIComponent: Component, DefaultInitializable, ObservableObject, ObservableObjectProvider { - @Dependency private var localStorage: LocalStorage - @Dependency private var secureStorage: SecureStorage - - /// The OpenAI GPT Model type that is used to interact with the OpenAI API - @AppStorage(OpenAIConstants.modelStorageKey) public var openAIModel: Model = .gpt3_5Turbo - private var defaultAPIToken: String? - - - /// The API token used to interact with the OpenAI API - public var apiToken: String? { - get { - try? secureStorage.retrieveCredentials(OpenAIConstants.credentialsUsername, server: OpenAIConstants.credentialsServer)?.password - } - set { - objectWillChange.send() - if let newValue { - try? secureStorage.store( - credentials: Credentials(username: OpenAIConstants.credentialsUsername, password: newValue), - server: OpenAIConstants.credentialsServer - ) - } else { - try? secureStorage.deleteCredentials(OpenAIConstants.credentialsUsername, server: OpenAIConstants.credentialsServer) - } - } - } - - - /// Initializes a new instance of `OpenAIGPT` with the specified API token and OpenAI model. - /// - /// - Parameters: - /// - apiToken: The API token for the OpenAI API. - /// - openAIModel: The OpenAI model to use for querying. - public init(apiToken: String? = nil, openAIModel model: Model? = nil) { - if UserDefaults.standard.object(forKey: OpenAIConstants.modelStorageKey) == nil { - self.openAIModel = openAIModel - } - - defaultAPIToken = apiToken - } - - public required convenience init() { - self.init(apiToken: nil, openAIModel: nil) - } - - - public func configure() { - if self.apiToken == nil, let defaultAPIToken { - self.apiToken = defaultAPIToken - } - } - - - /// Queries the OpenAI API using the provided messages. - /// - /// - Parameters: - /// - chat: A collection of chat messages used in the conversation. - /// - chatFunctionDeclaration: OpenAI functions that should be injected in the OpenAI query. - /// - /// - Returns: The content of the response from the API. - public func queryAPI( - withChat chat: [Chat], - withFunction chatFunctionDeclaration: [ChatFunctionDeclaration] = [] - ) async throws -> AsyncThrowingStream { - guard let apiToken, !apiToken.isEmpty else { - throw OpenAIError.noAPIToken - } - - let functions = chatFunctionDeclaration.isEmpty ? nil : chatFunctionDeclaration - - let openAIClient = OpenAI(apiToken: apiToken) - let query = ChatQuery(model: openAIModel, messages: chat, functions: functions) - return openAIClient.chatsStream(query: query) - } -} diff --git a/Sources/SpeziOpenAI/OpenAIModel.swift b/Sources/SpeziOpenAI/OpenAIModel.swift new file mode 100644 index 0000000..80c1fee --- /dev/null +++ b/Sources/SpeziOpenAI/OpenAIModel.swift @@ -0,0 +1,101 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import OpenAI +@_exported import struct OpenAI.Model +@_exported import struct OpenAI.ChatStreamResult +import Foundation +import Observation +import SpeziSecureStorage + + +/// View model responsible for to coordinate the interactions with the OpenAI GPT API. +@Observable +public class OpenAIModel { + private enum Defaults { + static let defaultModel: Model = .gpt3_5Turbo + } + + + private let secureStorage: SecureStorage + + + /// The OpenAI GPT Model type that is used to interact with the OpenAI API + public var openAIModel: String { + get { + access(keyPath: \.openAIModel) + return UserDefaults.standard.value(forKey: OpenAIConstants.modelStorageKey) as? Model ?? Defaults.defaultModel + } + set { + withMutation(keyPath: \.openAIModel) { + UserDefaults.standard.set(newValue, forKey: OpenAIConstants.modelStorageKey) + } + } + } + + /// The API token used to interact with the OpenAI API + public var apiToken: String? { + get { + access(keyPath: \.apiToken) + return try? secureStorage.retrieveCredentials(OpenAIConstants.credentialsUsername, server: OpenAIConstants.credentialsServer)?.password + } + set { + withMutation(keyPath: \.apiToken) { + if let newValue { + try? secureStorage.store( + credentials: Credentials(username: OpenAIConstants.credentialsUsername, password: newValue), + server: OpenAIConstants.credentialsServer + ) + } else { + try? secureStorage.deleteCredentials(OpenAIConstants.credentialsUsername, server: OpenAIConstants.credentialsServer) + } + } + } + } + + + init(secureStorage: SecureStorage, apiToken defaultToken: String? = nil, openAIModel model: Model? = nil) { + self.secureStorage = secureStorage + + if UserDefaults.standard.object(forKey: OpenAIConstants.modelStorageKey) == nil { + self.openAIModel = model ?? Defaults.defaultModel + } + + if let apiTokenFromStorage = try? secureStorage.retrieveCredentials( + OpenAIConstants.credentialsUsername, + server: OpenAIConstants.credentialsServer + )?.password { + self.apiToken = apiTokenFromStorage + } else { + self.apiToken = defaultToken + } + } + + + /// Queries the OpenAI API using the provided messages. + /// + /// - Parameters: + /// - chat: A collection of chat messages used in the conversation. + /// - chatFunctionDeclaration: OpenAI functions that should be injected in the OpenAI query. + /// + /// - Returns: The content of the response from the API. + public func queryAPI( + withChat chat: [Chat], + withFunction chatFunctionDeclaration: [ChatFunctionDeclaration] = [] + ) async throws -> AsyncThrowingStream { + guard let apiToken, !apiToken.isEmpty else { + throw OpenAIError.noAPIToken + } + + let functions = chatFunctionDeclaration.isEmpty ? nil : chatFunctionDeclaration + + let openAIClient = OpenAI(apiToken: apiToken) + let query = ChatQuery(model: openAIModel, messages: chat, functions: functions) + return openAIClient.chatsStream(query: query) + } +} diff --git a/Sources/SpeziOpenAI/OpenAIModelSelectionOnboardingStep.swift b/Sources/SpeziOpenAI/OpenAIModelSelectionOnboardingStep.swift index dd3c523..1437412 100644 --- a/Sources/SpeziOpenAI/OpenAIModelSelectionOnboardingStep.swift +++ b/Sources/SpeziOpenAI/OpenAIModelSelectionOnboardingStep.swift @@ -28,7 +28,7 @@ public struct OpenAIModelSelectionOnboardingStep: View { } - @EnvironmentObject private var openAI: OpenAIComponent + @Environment(OpenAIModel.self) private var openAI private let actionText: String private let action: () -> Void private let models: [ModelSelection] @@ -43,6 +43,7 @@ public struct OpenAIModelSelectionOnboardingStep: View { ) }, contentView: { + @Bindable var openAI = openAI Picker(String(localized: "OPENAI_MODEL_SELECTION_DESCRIPTION", bundle: .module), selection: $openAI.openAIModel) { ForEach(models) { model in Text(model.description) @@ -54,7 +55,7 @@ public struct OpenAIModelSelectionOnboardingStep: View { }, actionView: { OnboardingActionsView( - actionText, + verbatim: actionText, action: { action() } diff --git a/Sources/SpeziOpenAI/OpenAIModule.swift b/Sources/SpeziOpenAI/OpenAIModule.swift new file mode 100644 index 0000000..b716fc7 --- /dev/null +++ b/Sources/SpeziOpenAI/OpenAIModule.swift @@ -0,0 +1,43 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +import OpenAI +import Spezi +import SpeziSecureStorage + + +/// `OpenAIModule` is a module responsible for to coordinate the interactions with the OpenAI GPT API. +public class OpenAIModule: Module, DefaultInitializable { + @Module.Model private var model: OpenAIModel + @Dependency private var secureStorage: SecureStorage + + + private var defaultAPIToken: String? + private var defaultOpenAIModel: Model? + + + /// Initializes a new instance of `OpenAIGPT` with the specified API token and OpenAI model. + /// + /// - Parameters: + /// - apiToken: The API token for the OpenAI API. + /// - openAIModel: The OpenAI model to use for querying. + public init(apiToken: String? = nil, openAIModel: Model? = nil) { + defaultAPIToken = apiToken + defaultOpenAIModel = openAIModel + } + + public required convenience init() { + self.init(apiToken: nil, openAIModel: nil) + } + + + public func configure() { + self.model = OpenAIModel(secureStorage: secureStorage, apiToken: defaultAPIToken, openAIModel: defaultOpenAIModel) + } +} diff --git a/Sources/SpeziOpenAI/SpeziOpenAI.docc/SpeziOpenAI.md b/Sources/SpeziOpenAI/SpeziOpenAI.docc/SpeziOpenAI.md index 3ddc682..bbb0244 100644 --- a/Sources/SpeziOpenAI/SpeziOpenAI.docc/SpeziOpenAI.md +++ b/Sources/SpeziOpenAI/SpeziOpenAI.docc/SpeziOpenAI.md @@ -40,11 +40,12 @@ First, you will need to add the SpeziML Swift package to [your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or [Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). When adding the package, select the `SpeziOpenAI` target to add. -### 2. Register the Open AI Component +### 2. Register the Open AI Module -> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure. +> Note: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure. -You can configure the `OpenAIComponent` in the `SpeziAppDelegate` as follows. +You can configure the ``OpenAIModule`` in the `SpeziAppDelegate` as follows. +In the example, we configure the `OpenAIModule` to use the GPT-4 model with a default API key. ```swift import Spezi @@ -54,17 +55,30 @@ import SpeziOpenAI class ExampleDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - OpenAIComponent(apiToken: "API_KEY", openAIModel: .gpt4) + OpenAIModule(apiToken: "API_KEY", openAIModel: .gpt4) } } } ``` -In the example above, we have configured the `OpenAIComponent` to use the GPT-4 model with a default API key. Note that the choice of model and API key are persisted across application launches. The `apiToken` and `openAIModel` can also be accessed and changed at runtime. +The OpenAIModule injects an ``OpenAIModel`` in the SwiftUI environment to make it accessible thoughout your application. + +```swift +class ExampleView: View { + @Environment(OpenAIModel.self) var model + + + var body: some View { + // ... + } +} +``` + +> Tip: The choice of model and API key are persisted across application launches. The `apiToken` and `openAIModel` can also be accessed and changed at runtime. The `SpeziOpenAI` package also provides an `OpenAIAPIKeyOnboardingStep` that can be used to allow the user to provide their API key during the onboarding process instead (see `Examples` below). If using the `OpenAIAPIKeyOnboardingStep`, the `apiToken` property can be omitted here. -> You can learn more about a [`Component` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component). +> Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). ## Examples @@ -78,13 +92,13 @@ import SpeziOpenAI import SwiftUI struct OpenAIChatView: View { - @EnvironmentObject private var openAIComponent: OpenAIComponent + @Environment(OpenAIModel.self) var model @State private var chat: [Chat] var body: some View { ChatView($chat) .onChange(of: chat) { _ in - let chatStreamResults = try await openAIComponent.queryAPI(withChat: chat) + let chatStreamResults = try await model.queryAPI(withChat: chat) for try await chatStreamResult in chatStreamResults { for choice in chatStreamResult.choices { @@ -153,12 +167,3 @@ struct OnboardingFlow: View { ``` Now the OpenAI API Key entry view will appear within your application's onboarding process. The API Key entered will be persisted across application launches. - - - -## Types - -### Open AI GPT - -- ``OpenAIGPT`` -- ``OpenAIGPTError`` diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index f88b68a..2a42bde 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -14,7 +14,7 @@ import XCTSpezi class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - OpenAIComponent() + OpenAIModule() } } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 78cb298..bc10467 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -11,9 +11,9 @@ 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; - 2FC186052AD52C5B0065EBB2 /* SpeziSpeechSynthesizer in Frameworks */ = {isa = PBXBuildFile; productRef = 2FC186042AD52C5B0065EBB2 /* SpeziSpeechSynthesizer */; }; 2FC186012AD528830065EBB2 /* SpeziOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 2FC186002AD528830065EBB2 /* SpeziOpenAI */; }; 2FC186032AD528830065EBB2 /* SpeziSpeechRecognizer in Frameworks */ = {isa = PBXBuildFile; productRef = 2FC186022AD528830065EBB2 /* SpeziSpeechRecognizer */; }; + 2FC186052AD52C5B0065EBB2 /* SpeziSpeechSynthesizer in Frameworks */ = {isa = PBXBuildFile; productRef = 2FC186042AD52C5B0065EBB2 /* SpeziSpeechSynthesizer */; }; 2FD590472A19E42000153BE4 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD590452A19E41D00153BE4 /* TestAppDelegate.swift */; }; 2FD590492A19E4AE00153BE4 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2FD590482A19E4AE00153BE4 /* Spezi */; }; 2FD5904B2A19E4AE00153BE4 /* XCTSpezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2FD5904A2A19E4AE00153BE4 /* XCTSpezi */; }; @@ -327,7 +327,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -381,7 +381,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -547,7 +547,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -651,7 +651,7 @@ repositoryURL = "https://github.com/StanfordSpezi/Spezi.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.7.3; + minimumVersion = 0.8.0; }; }; 2FD590502A19E9F000153BE4 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = {