Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Sources/FOSMVVM/Forms/FormField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,7 @@ public extension FormField {
switch type {
case .text(let inputType): inputType.textContentType
case .textArea(let inputType): inputType.textContentType
case .textArray(let inputType): inputType.textContentType
case .duration, .checkbox, .colorPicker, .select, .multipleRows, .multipleColumns: nil
case .checkbox, .colorPicker, .select: nil
}
}
}
Expand Down
44 changes: 27 additions & 17 deletions Sources/FOSMVVM/Localization/YamlLocalizationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public enum YamlStoreError: Error {
case yamlError(error: YamlError)
case fileError(path: URL, error: any Error)
case fileDecodingError(path: URL)
case noPaths
case noResourcePaths
case noLocalizationStore
case noLocaleFound

Expand All @@ -33,7 +33,7 @@ public enum YamlStoreError: Error {
case .yamlError(let error): "YamlStoreError: \(error)"
case .fileError(let path, let error): "YamlStoreError: File read error at: \(path) - \(error)"
case .fileDecodingError(let path): "YamlStoreError: \(path) -- Unable to decode contents of file to UTF-8"
case .noPaths:
case .noResourcePaths:
"YamlStoreError: No YAML search paths were provided or the paths provided didn't match any real files or directories."
case .noLocalizationStore:
"YamlStoreError: The LocalizationStore is missing from the Application. Generally this " +
Expand All @@ -59,38 +59,48 @@ public extension Bundle {
}
}

public extension Collection<Bundle> {
func yamlLocalization(resourceDirectoryName: String) async throws -> LocalizationStore {
let searchPaths = reduce(into: Set<URL>()) { result, next in
result.formUnion(next.yamlSearchPaths(resourceDirectoryName: resourceDirectoryName))
}

let config = YamlStoreConfig(
searchPaths: searchPaths
)

return try await YamlStore(config: config)
}
}

package struct YamlStoreConfig: Sendable { // Internal for testing
let searchPaths: [URL]

fileprivate func localizationStore() async throws -> LocalizationStore {
try await YamlStore(config: self)
}

init(searchPaths: some Collection<URL>) throws {
guard !searchPaths.isEmpty else {
throw YamlStoreError.noPaths
}

init(searchPaths: some Collection<URL>) {
self.searchPaths = searchPaths.filter {
$0.isFileURL && $0.hasDirectoryPath
}

guard !self.searchPaths.isEmpty else {
throw YamlStoreError.noPaths
}
}
}

package extension Bundle {
func yamlStoreConfig(resourceDirectoryName: String) throws -> YamlStoreConfig {
try .init(
searchPaths: yamlSearchPaths(
resourceDirectoryName: resourceDirectoryName
)
let searchPaths = yamlSearchPaths(
resourceDirectoryName: resourceDirectoryName
)

guard !searchPaths.isEmpty else {
throw YamlStoreError.noResourcePaths
}

return .init(searchPaths: searchPaths)
}

func yamlSearchPaths(resourceDirectoryName: String) throws -> Set<URL> {
func yamlSearchPaths(resourceDirectoryName: String) -> Set<URL> {
let resourceURLs = [
/* Packages */ bundleURL
.appending(path: "Contents/Resources")
Expand Down Expand Up @@ -175,7 +185,7 @@ private extension YamlStore {
var result: [String: [String: YamlValue]] = [:]

guard !config.searchPaths.isEmpty else {
throw YamlStoreError.noPaths
throw YamlStoreError.noResourcePaths
}

for searchPath in config.searchPaths {
Expand Down
4 changes: 4 additions & 0 deletions Sources/FOSMVVM/SwiftUI Support/FormFieldView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ private extension FormFieldView where Value == String? {

private extension FormFieldView where Value == Date {
func dateFieldView(onNewValue: ((Value) -> Void)?, onSubmit: ((Value) -> Void)?) -> some View {
#if !os(tvOS)
DatePicker(
selection: .init(
get: { fieldModel.wrappedValue },
Expand All @@ -511,6 +512,9 @@ private extension FormFieldView where Value == Date {
label: { Text(fieldModel.formField.title) }
)
.datePickerStyle(DefaultDatePickerStyle())
#else
Text("DateField not supported on tvOS")
#endif
}
}

Expand Down
36 changes: 28 additions & 8 deletions Sources/FOSMVVM/SwiftUI Support/MVVMEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ public final class MVVMEnvironment: @unchecked Sendable {
}
}

public let resourceBundles: [Bundle]
public let resourceDirectoryName: String?
private let localizationStore: LocalizationStore?

typealias ViewFactory = (Data) throws -> AnyView

Expand Down Expand Up @@ -117,10 +119,14 @@ public final class MVVMEnvironment: @unchecked Sendable {
return store
}

let localizationStore = try await Bundle.main.yamlLocalization(
resourceDirectoryName: resourceDirectoryName ?? ""
)
_clientLocalizationStore = localizationStore
let locStore: LocalizationStore = if let localizationStore {
localizationStore
} else {
try await resourceBundles.yamlLocalization(
resourceDirectoryName: resourceDirectoryName ?? ""
)
}
_clientLocalizationStore = locStore
return localizationStore
}
}
Expand Down Expand Up @@ -162,6 +168,7 @@ public final class MVVMEnvironment: @unchecked Sendable {
/// - Parameters:
/// - currentVersion: The current SystemVersion of the application (default: see note)
/// - appBundle: The application's *Bundle* (e.g. *Bundle.main*)
/// - resourceBundles: All *Bundle*s that contain YAML resources (default: appBundle)
/// - resourceDirectoryName: The directory name that contains the resources (default: nil). Only needed
/// if the client application is hosting the YAML files.
/// - requestHeaders: A set of HTTP header fields for the URLRequest
Expand All @@ -173,12 +180,15 @@ public final class MVVMEnvironment: @unchecked Sendable {
@MainActor public init(
currentVersion: SystemVersion? = nil,
appBundle: Bundle,
resourceBundles: [Bundle]? = nil,
resourceDirectoryName: String? = nil,
requestHeaders: [String: String] = [:],
deploymentURLs: [Deployment: URLPackage],
requestErrorHandler: (@Sendable (any ServerRequest, any ServerRequestError) -> Void)? = nil,
loadingView: (@Sendable () -> AnyView)? = nil
) {
self.localizationStore = nil
self.resourceBundles = resourceBundles ?? [appBundle]
self.resourceDirectoryName = resourceDirectoryName
self.requestHeaders = requestHeaders
self.deploymentURLs = deploymentURLs
Expand All @@ -200,6 +210,7 @@ public final class MVVMEnvironment: @unchecked Sendable {
/// - Parameters:
/// - currentVersion: The current SystemVersion of the application (default: see note)
/// - appBundle: The applications *Bundle* (e.g. *Bundle.main*)
/// - resourceBundles: All *Bundle*s that contain YAML resources (default: appBundle)
/// - resourceDirectoryName: The directory name that contains the resources (default: nil). Only needed
/// if the client application is hosting the YAML files.
/// - requestHeaders: A set of HTTP header fields for the URLRequest
Expand All @@ -211,6 +222,7 @@ public final class MVVMEnvironment: @unchecked Sendable {
@MainActor public convenience init(
currentVersion: SystemVersion? = nil,
appBundle: Bundle,
resourceBundles: [Bundle]? = nil,
resourceDirectoryName: String? = nil,
requestHeaders: [String: String] = [:],
deploymentURLs: [Deployment: URL],
Expand All @@ -220,6 +232,7 @@ public final class MVVMEnvironment: @unchecked Sendable {
self.init(
currentVersion: currentVersion,
appBundle: appBundle,
resourceBundles: resourceBundles,
resourceDirectoryName: resourceDirectoryName,
requestHeaders: requestHeaders,
deploymentURLs: deploymentURLs.reduce([Deployment: URLPackage]()) { result, pair in
Expand All @@ -236,10 +249,17 @@ public final class MVVMEnvironment: @unchecked Sendable {

/// Initializes ``MVVMEnvironment`` for previews
///
/// > This overload does **NOT** check the application's version as it is not necessary
/// > for previews
@MainActor init(resourceDirectoryName: String? = nil, deploymentURLs: [Deployment: URLPackage], loadingView: (@Sendable () -> AnyView)? = nil) {
self.resourceDirectoryName = resourceDirectoryName
/// > This overload does **NOT** check the application's version as it is not necessary for previews
@MainActor init(
localizationStore: LocalizationStore,
deploymentURLs: [Deployment: URLPackage],
loadingView: (
@Sendable () -> AnyView
)? = nil
) {
self.localizationStore = localizationStore
self.resourceBundles = []
self.resourceDirectoryName = nil
self.requestHeaders = [:]
self.deploymentURLs = deploymentURLs
self.requestErrorHandler = nil
Expand Down
17 changes: 11 additions & 6 deletions Sources/FOSMVVM/SwiftUI Support/PreviewHostingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ private extension ViewModelView {
}

private struct PreviewHostingView<Inner: ViewModelView>: View {
@State private var loadingText = "Loading Localization for Preview..."
@State private var localizationStore: LocalizationStore?

let inner: Inner.Type
Expand All @@ -135,25 +136,29 @@ private struct PreviewHostingView<Inner: ViewModelView>: View {
viewModel: viewModel
))
.setStates(modifier: setStates ?? { _ in () })
.preferredColorScheme(ColorScheme.light)
.environment(mmEnv(resourceDirectoryName: resourceDirectoryName))
.environment(mmEnv(localizationStore: localizationStore))
} else {
Text("Loading...")
Text(loadingText)
.task {
do {
localizationStore = try await bundle.yamlLocalization(
resourceDirectoryName: resourceDirectoryName
)
} catch {
fatalError("Unable to initialize the localization store: \(error)")
loadingText = """
Unable to initialize the localization store: \(error)

- Bundle: \(bundle.bundlePath)
- resourceDirectoryName: \(resourceDirectoryName.isEmpty ? "<Empty>" : resourceDirectoryName)
"""
}
}
}
}

private func mmEnv(resourceDirectoryName: String) -> MVVMEnvironment {
private func mmEnv(localizationStore: LocalizationStore) -> MVVMEnvironment {
MVVMEnvironment(
resourceDirectoryName: resourceDirectoryName,
localizationStore: localizationStore,
deploymentURLs: [
.debug: .init(serverBaseURL: URL(string: "https://localhost:8080")!)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Vapor
struct YamlLocalizationStoreInitTests {
@Test func yamlStoreConfig() throws {
let paths = paths
let config = try YamlStoreConfig(searchPaths: paths)
let config = YamlStoreConfig(searchPaths: paths)
#expect(config.searchPaths.count == paths.count)
}

Expand Down