Skip to content

Commit

Permalink
Introduce SpeziPersonalInfo target (#20)
Browse files Browse the repository at this point in the history
# Introduce SpeziPersonalInfo target

## ♻️ Current situation & Problem
Previously, `SpeziViews` was treated like a catch all for any
UI-component-based API interfaces. This PR introduces `SpeziPersonInfo`
that paves the way for a more granular grouping of UI components. In
this PR we move out any UI components that deal with personal
information. There might be future additions coming from a SpeziAccount
refactoring.

This brings some breaking changes, where we removed the `NameFields`
view and instead replaced it with new optimized views `NameTextField`
and `NameFieldRow`. These address specific parts of a
`PersonNameComponents` and remove a lot of the complexity like focus
state handling. As you now can directly access the individual
TextFields, this can be more cleanly handled by the users themselves.

Further this PR removes the unused `HTMLView` (and `DocumentView`).
`MarkdownView` remains.

This PR updates the project to target a deployment target of iOS 17 and
migrates to use the new String Catalogs. We made sure to use the
explicit `init(verbatim:)` initializer for `Text` views where necessary.
We exposed similar functionality with our `LazyText` und `Label` views.
External parameters were changed and induce a breaking change.

## ⚙️ Release Notes 
* The new `SpeziPersonalInfo` target is the entry point for all View
components that deal with personal information.
* The `NameFields` view was removed and replaced with row-based views
`NameTextField` and `NameFieldRow`.
* The `HTMLView` and `DocumentView` were removed.
* New initializer argument labels for `LazyText` and `Label` views.
* Added Documentation Catalogs to structure documentation more clearly.
* Upgraded to use String Catalogs.
* Updated target platform to iOS 17.


## 📚 Documentation
The PR adds documentation catalogs to both targets which were previously
missing. Through these documentation catalogs we create a structure that
allows to more easily explore SpeziViews and SpeziPersonalInfo packages.
Certain documentation was optimized to more clearly communicate the
technical details.


## ✅ Testing
Testing was update to separate functionality of the two new targets to
provide a clear separation between both.

## 📝 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).
  • Loading branch information
Supereg authored Nov 3, 2023
1 parent 4b7cc42 commit b27cb60
Show file tree
Hide file tree
Showing 48 changed files with 1,002 additions and 903 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ jobs:
name: Build and Test Swift Package
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
artifactname: SpeziViews.xcresult
artifactname: SpeziViews-Package.xcresult
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziViews
scheme: SpeziViews-Package
buildandtestuitests:
name: Build and Test UI Tests
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand All @@ -36,4 +36,4 @@ jobs:
needs: [buildandtest, buildandtestuitests]
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
with:
coveragereports: SpeziViews.xcresult TestApp.xcresult
coveragereports: SpeziViews-Package.xcresult TestApp.xcresult
16 changes: 12 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.7
// swift-tools-version:5.9

//
// This source file is part of the Stanford Spezi open-source project
Expand All @@ -15,19 +15,27 @@ let package = Package(
name: "SpeziViews",
defaultLocalization: "en",
platforms: [
.iOS(.v16)
.iOS(.v17)
],
products: [
.library(name: "SpeziViews", targets: ["SpeziViews"])
.library(name: "SpeziViews", targets: ["SpeziViews"]),
.library(name: "SpeziPersonalInfo", targets: ["SpeziPersonalInfo"])
],
targets: [
.target(
name: "SpeziViews"
),
.target(
name: "SpeziPersonalInfo",
dependencies: [
.target(name: "SpeziViews")
]
),
.testTarget(
name: "SpeziViewsTests",
dependencies: [
.target(name: "SpeziViews")
.target(name: "SpeziViews"),
.target(name: "SpeziPersonalInfo")
]
)
]
Expand Down
139 changes: 139 additions & 0 deletions Sources/SpeziPersonalInfo/Fields/NameFieldRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SpeziViews
import SwiftUI


/// A `NameTextField` that always shows a description in front of the text field.
///
/// The `NameFieldRow` uses the `DescriptionGridRow` and is to be placed into a [Grid](https://developer.apple.com/documentation/swiftui/grid)
/// view to provide a description text in front of the ``NameTextField``.
///
/// Below is a short code example on how to collect both the given name and family name of a person within a SwiftUI `Form`.
/// ```swift
/// @State private var name = PersonNameComponents()
///
/// var body: some View {
/// Form {
/// Grid(horizontalSpacing: 15) { // optional horizontal spacing
/// NameFieldRow(name: $name, for: \.givenName) {
/// Text(verbatim: "First")
/// } label: {
/// Text(verbatim: "enter first name")
/// }
///
/// Divider()
/// .gridCellUnsizedAxes(.horizontal)
///
/// NameFieldRow(name: $name, for: \.familyName) {
/// Text(verbatim: "Last")
/// } label: {
/// Text(verbatim: "enter last name")
/// }
/// }
/// }
/// }
/// ```
public struct NameFieldRow<Description: View, Label: View>: View {
private let description: Description
private let label: Label
private let component: WritableKeyPath<PersonNameComponents, String?>

@Binding private var name: PersonNameComponents


public var body: some View {
DescriptionGridRow {
description
} content: {
NameTextField(name: $name, for: component) {
label
}
}
}


/// Creates a name text field with a description label.
/// - Parameters:
/// - description: The localized description label displayed before the text field.
/// - name: The name to display and edit.
/// - component: The `KeyPath` to the property of the provided `PersonNameComponents` to display and edit.
/// - label: A view that describes the purpose of the text field.
public init(
_ description: LocalizedStringResource,
name: Binding<PersonNameComponents>,
for component: WritableKeyPath<PersonNameComponents, String?>,
@ViewBuilder label: () -> Label
) where Description == Text {
self.init(name: name, for: component, description: { Text(description) }, label: label)
}

/// Creates a name text field with a description label.
/// - Parameters:
/// - name: The name to display and edit.
/// - component: The `KeyPath` to the property of the provided `PersonNameComponents` to display and edit.
/// - prompt: An optional `Text` prompt. Refer to the documentation of `TextField` for more information.
/// - description: The description label displayed before the text field.
/// - label: A view that describes the purpose of the text field.
public init(
name: Binding<PersonNameComponents>,
for component: WritableKeyPath<PersonNameComponents, String?>,
@ViewBuilder description: () -> Description,
@ViewBuilder label: () -> Label
) {
self._name = name
self.component = component
self.description = description()
self.label = label()
}
}


#if DEBUG
#Preview {
@State var name = PersonNameComponents()
return Grid(horizontalSpacing: 15) {
NameFieldRow(name: $name, for: \.familyName) {
Text(verbatim: "First")
} label: {
Text(verbatim: "enter first name")
}

Divider()
.gridCellUnsizedAxes(.horizontal)

NameFieldRow(name: $name, for: \.familyName) {
Text(verbatim: "Last")
} label: {
Text(verbatim: "enter last name")
}
}
}
#Preview {
@State var name = PersonNameComponents()
return Form {
Grid(horizontalSpacing: 15) {
NameFieldRow(name: $name, for: \.givenName) {
Text(verbatim: "First")
} label: {
Text(verbatim: "enter first name")
}

Divider()
.gridCellUnsizedAxes(.horizontal)

NameFieldRow(name: $name, for: \.familyName) {
Text(verbatim: "Last")
} label: {
Text(verbatim: "enter last name")
}
}
}
}
#endif
122 changes: 122 additions & 0 deletions Sources/SpeziPersonalInfo/Fields/NameTextField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


/// A TextField for properties of `PersonNameComponents`.
///
/// The `NameTextField` view allows to create a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) for properties
/// of [PersonNameComponents](https://developer.apple.com/documentation/foundation/personnamecomponents).
/// To do so you supply a Binding to your `PersonNameComponents` value and a `KeyPath` to the property of `PersonNameComponents` you are trying to input.
///
/// `NameTextField` modifies the underlying `TextField` to optimize for name entry and automatically sets modifiers like
/// [textContentType(_:)](https://developer.apple.com/documentation/swiftui/view/textcontenttype(_:)-ufdv).
///
/// Below is a short code example on how to create an editable text interface for the given name of a person.
/// ```swift
/// @State private var name = PersonNameComponents()
///
/// var body: some View {
/// NameTextField("enter first name", name: $name, for: \.givenName)
/// }
/// ```
///
/// - Note: A empty string will be automatically mapped to a `nil` value for the respective property of `PersonNameComponents`.
public struct NameTextField<Label: View>: View {
private let prompt: Text?
private let label: Label
private let nameComponent: WritableKeyPath<PersonNameComponents, String?>

@Binding private var name: PersonNameComponents

private var componentBinding: Binding<String> {
Binding {
name[keyPath: nameComponent] ?? ""
} set: { newValue in
name[keyPath: nameComponent] = newValue.isEmpty ? nil : newValue
}
}

private var contentType: UITextContentType? {
switch nameComponent {
case \.namePrefix:
return .namePrefix
case \.nameSuffix:
return .nameSuffix
case \.givenName:
return .givenName
case \.middleName:
return .middleName
case \.familyName:
return .familyName
case \.nickname:
return .nickname
default:
return .name // general, catch all content type
}
}


public var body: some View {
TextField(text: componentBinding, prompt: prompt) {
label
}
.autocorrectionDisabled()
.textInputAutocapitalization(.words)
.textContentType(contentType)
}


/// Creates a name text field with an optional prompt.
/// - Parameters:
/// - label: A localized title of the text field, describing its purpose.
/// - name: The name to display and edit.
/// - component: The `KeyPath` to the property of the provided `PersonNameComponents` to display and edit.
/// - prompt: An optional `Text` prompt. Refer to the documentation of `TextField` for more information.
public init(
_ label: LocalizedStringResource,
name: Binding<PersonNameComponents>,
for component: WritableKeyPath<PersonNameComponents, String?>,
prompt: Text? = nil
) where Label == Text {
self.init(name: name, for: component, prompt: prompt) {
Text(label)
}
}

/// Creates a name text field with an optional prompt.
/// - Parameters:
/// - name: The name to display and edit.
/// - component: The `KeyPath` to the property of the provided `PersonNameComponents` to display and edit.
/// - prompt: An optional `Text` prompt. Refer to the documentation of `TextField` for more information.
/// - label: A view that describes the purpose of the text field.
public init(
name: Binding<PersonNameComponents>,
for component: WritableKeyPath<PersonNameComponents, String?>,
prompt: Text? = nil,
@ViewBuilder label: () -> Label
) {
self._name = name
self.nameComponent = component
self.prompt = prompt
self.label = label()
}
}


#if DEBUG
#Preview {
@State var name = PersonNameComponents()
return List {
NameTextField(name: $name, for: \.givenName) {
Text(verbatim: "enter first name")
}
}
}
#endif
7 changes: 7 additions & 0 deletions Sources/SpeziPersonalInfo/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sourceLanguage" : "en",
"strings" : {

},
"version" : "1.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

This source file is part of the Stanford Spezi open-source project

SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)

SPDX-License-Identifier: MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ``SpeziPersonalInfo``

A SpeziViews target that provides a common set of SwiftUI views and related functionality for managing personal information.

<!--
This source file is part of the Spezi open-source project
SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
SPDX-License-Identifier: MIT
-->

## Topics

### Person Name

- ``NameTextField``
- ``NameFieldRow``

### User Profile

- ``UserProfileView``
Loading

0 comments on commit b27cb60

Please sign in to comment.