Skip to content

Commit

Permalink
Improve ContactList layout and improve subtitle line breaking (#18)
Browse files Browse the repository at this point in the history
# Improve ContactList layout and improve subtitle line breaking

## ♻️ Current situation & Problem
Currently, the `ContactsList` view is built using a custom ScrollView
implementation and drawing custom backgrounds with shades around the
`ContactView`s. This doesn't really feel native on iOS. Instead, this PR
rethinks the implementation by rebuilding the `ContactsList` view using
SwiftUI standard components like `List`. This makes the `ContactsList`
now feel right at home.
Further, previously the subtitle, consisting of the Person's title and
organization, was built using multiple distinct `Text` instances. This
caused problems when the text was longer than the view was capable of
displaying. This PR ensures that a persons title and organization are
combined into a single `Text` view and allows to to wrap into a second
line.
Lastly, this PR migrates the PR to use String catalogs, optimizes key
naming and bumps the target to iOS 17.

Below are two screenshots comparing the previous implementation to the
updated one.

<img width="380" alt="Bildschirmfoto 2023-11-01 um 22 13 11"
src="https://github.com/StanfordSpezi/SpeziContact/assets/9783857/18eff6c1-b105-4f92-b0ae-19b28e9dc044">
<img width="380" alt="Bildschirmfoto 2023-11-01 um 22 12 14"
src="https://github.com/StanfordSpezi/SpeziContact/assets/9783857/1111a371-8193-4724-9d10-f1feaed28d7f">

## ⚙️ Release Notes 
* New updated visuals for the `ContactsView` that feel way more native
to iOS
* Updated line breaking behavior for too long contact subtitles (title
and organization)
* Migrated to use String Catalogs for localization


## 📚 Documentation
--


## ✅ Testing
Tests were slightly adjusted, due to issues with line breaks in the
labels.


## 📝 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 committed Nov 5, 2023
1 parent 9dea670 commit bd38bd7
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 134 deletions.
9 changes: 5 additions & 4 deletions Package.swift
@@ -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,20 @@ let package = Package(
name: "SpeziContact",
defaultLocalization: "en",
platforms: [
.iOS(.v16)
.iOS(.v17)
],
products: [
.library(name: "SpeziContact", targets: ["SpeziContact"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.5.0"))
.package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.6.0"))
],
targets: [
.target(
name: "SpeziContact",
dependencies: [
.product(name: "SpeziViews", package: "SpeziViews")
.product(name: "SpeziViews", package: "SpeziViews"),
.product(name: "SpeziPersonalInfo", package: "SpeziViews")
]
),
.testTarget(
Expand Down
41 changes: 0 additions & 41 deletions Sources/SpeziContact/Contact Views/ContactCard.swift

This file was deleted.

51 changes: 32 additions & 19 deletions Sources/SpeziContact/Contact Views/ContactView.swift
Expand Up @@ -8,6 +8,7 @@

import Contacts
import MessageUI
import SpeziPersonalInfo
import SpeziViews
import SwiftUI

Expand All @@ -26,29 +27,43 @@ public struct ContactView: View {
return (contact.contactOptions.dropLast(leftOverElements), contact.contactOptions.dropFirst(columnCount * numberOfRows))
}

private var subtitleLabel: Text {
private var subtitleText: Text {
var text = Text(verbatim: "")
if let title = contact.title {
text = text + Text(verbatim: title) // swiftlint:disable:this shorthand_operator
}
if contact.title != nil && contact.organization != nil {
text = text + Text(verbatim: " - ") // swiftlint:disable:this shorthand_operator
}
if let organization = contact.organization {
text = text + Text(verbatim: organization) // swiftlint:disable:this shorthand_operator
}
return text
}

private var subtitleAccessibilityLabel: Text {
var text: Text?
if let title = contact.title {
text = Text(verbatim: title)
}

if let organization = contact.organization {
if let titleText = text {
text = titleText + Text(" ") + Text("TITLE_AT_ORG", bundle: .module) + Text(" ") + Text(verbatim: organization)
text = titleText + Text(" at \(organization)", bundle: .module, comment: "Accessibility label: ' at <organization>'")
} else {
text = Text(verbatim: organization)
}
}

return text ?? Text(verbatim: "")
}

public var body: some View {
VStack {
header
Divider()
if let description = contact.description {
Label(description, textStyle: .subheadline)
Label(verbatim: description, textStyle: .subheadline)
.padding(.vertical, 4)
}
HorizontalGeometryReader { _ in
Expand All @@ -60,7 +75,7 @@ public struct ContactView: View {
addressButton
}
}

private var header: some View {
HStack(spacing: 0) {
UserProfileView(name: contact.name) {
Expand All @@ -74,25 +89,19 @@ public struct ContactView: View {
let name = contact.name.formatted(.name(style: .long))
Text(verbatim: name)
.font(.title3.bold())
.accessibilityLabel(Text("CONTACT \(name)", bundle: .module))
.accessibilityLabel(Text("Contact: \(name)", bundle: .module, comment: "Accessibility Label"))
.accessibilityAddTraits(.isHeader)

if contact.title != nil || contact.organization != nil {
HStack(spacing: 0) {
if let title = contact.title {
Text(verbatim: title)
}
if contact.title != nil && contact.organization != nil {
Text(" - ")
}
if let organization = contact.organization {
Text(verbatim: organization)
}
subtitleText
.lineLimit(1...2)
.fixedSize(horizontal: false, vertical: true)
}
.foregroundColor(Color(.secondaryLabel))
.font(.subheadline)
.accessibilityRepresentation {
subtitleLabel
subtitleAccessibilityLabel
}
}
}
Expand All @@ -104,7 +113,7 @@ public struct ContactView: View {
private var contactSection: some View {
VStack(spacing: 8) {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 100))],
columns: [GridItem(.adaptive(minimum: 95))],
alignment: .center,
spacing: 8
) {
Expand Down Expand Up @@ -135,7 +144,7 @@ public struct ContactView: View {
.foregroundStyle(Color(uiColor: .secondarySystemBackground))
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
Text("CONTACT_ADDRESS", bundle: .module)
Text("Address", bundle: .module, comment: "Contact Button Title")
.foregroundColor(.accentColor)
Text(verbatim: CNPostalAddressFormatter().string(from: address))
.multilineTextAlignment(.leading)
Expand All @@ -151,7 +160,11 @@ public struct ContactView: View {
}
.fixedSize(horizontal: false, vertical: true)
}
.accessibilityLabel(Text("ADDRESS_NAVIGATE \(Text(verbatim: CNPostalAddressFormatter().string(from: address)))", bundle: .module))
.accessibilityLabel(Text(
"Address: \(Text(verbatim: CNPostalAddressFormatter().string(from: address)))",
bundle: .module,
comment: "Accessibility Label"
))
} else {
EmptyView()
}
Expand Down
13 changes: 6 additions & 7 deletions Sources/SpeziContact/Contact Views/ContactsList.swift
Expand Up @@ -22,15 +22,14 @@ public struct ContactsList: View {


public var body: some View {
ScrollView(.vertical) {
List {
ForEach(contacts, id: \.id) { contact in
ContactCard(contact: contact)
.padding(.horizontal)
.padding(.vertical, 6)
Section {
ContactView(contact: contact)
.buttonStyle(.plain) // ensure the whole list row doesn't render as a button
}
}
.padding(.vertical, 6)
}
.background(Color(.systemGroupedBackground))
}


Expand All @@ -53,7 +52,7 @@ struct ContactsList_Previews: PreviewProvider {
ContactView_Previews.mock
]
)
.navigationTitle("Contacts")
.navigationTitle(Text(verbatim: "Contacts"))
.background(Color(.systemGroupedBackground))
}
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/SpeziContact/Models/Contact.swift
Expand Up @@ -19,7 +19,7 @@ public struct Contact {
public let image: Image?
/// The title of the individual.
public let title: String?
/// The desciption of the individual.
/// The description of the individual.
public let description: String?
/// The organization of the individual.
public let organization: String?
Expand All @@ -30,11 +30,11 @@ public struct Contact {


/// - Parameters:
/// - id: Identiifer of the `Contact` instance.
/// - id: Identifier of the `Contact` instance.
/// - name: The name of the individual. Ideally provide at least a first and given name.
/// - image: The image of the ``Contact``.
/// - title: The title of the individual.
/// - description: The desciption of the individual.
/// - description: The description of the individual.
/// - organization: The organization of the individual.
/// - address: The address of the individual.
/// - contactOptions: The contact options of the individual.
Expand Down
24 changes: 14 additions & 10 deletions Sources/SpeziContact/Models/ContactOption.swift
Expand Up @@ -51,12 +51,12 @@ extension ContactOption {
public static func call(_ number: String) -> ContactOption {
ContactOption(
image: Image(systemName: "phone.fill"),
title: String(localized: "CONTACT_OPTION_CALL", bundle: .module)
title: String(localized: "Call", bundle: .module, comment: "Contact Option")
) {
guard let url = URL(string: "tel://\(number)"), UIApplication.shared.canOpenURL(url) else {
presentAlert(
title: String(localized: "CONTACT_OPTION_CALL", bundle: .module),
message: String(localized: "CONTACT_OPTION_CALL_MANUAL \(number)", bundle: .module)
title: String(localized: "Call", bundle: .module),
message: String(localized: "Call unavailable. You can manually reach out to \(number)", bundle: .module, comment: "Call unavailable. Manual approach.")
)
return
}
Expand All @@ -69,12 +69,12 @@ extension ContactOption {
public static func text(_ number: String) -> ContactOption {
ContactOption(
image: Image(systemName: "message.fill"),
title: String(localized: "CONTACT_OPTION_TEXT", bundle: .module)
title: String(localized: "Text", bundle: .module, comment: "Contact Option")
) {
guard let url = URL(string: "sms:\(number)"), UIApplication.shared.canOpenURL(url) else {
presentAlert(
title: String(localized: "CONTACT_OPTION_TEXT", bundle: .module),
message: String(localized: "CONTACT_OPTION_TEXT_MANUAL \(number)", bundle: .module)
title: String(localized: "Text", bundle: .module),
message: String(localized: "Text unavailable. You can manually reach out to \(number)", bundle: .module, comment: "Text unavailable. Manual approach.")
)
return
}
Expand All @@ -90,13 +90,17 @@ extension ContactOption {
public static func email(addresses: [String], subject: String? = nil) -> ContactOption {
ContactOption(
image: Image(systemName: "envelope.fill"),
title: String(localized: "CONTACT_OPTION_EMAIL", bundle: .module)
title: String(localized: "Email", bundle: .module, comment: "Contact Option")
) {
guard let subject = (subject ?? "").addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "mailto:\(addresses.joined(separator: ";"))?subject=\(subject)"), UIApplication.shared.canOpenURL(url) else {
presentAlert(
title: String(localized: "CONTACT_OPTION_EMAIL", bundle: .module),
message: String(localized: "CONTACT_OPTION_EMAIL_MANUAL \(addresses.joined(separator: ", "))", bundle: .module)
title: String(localized: "Email", bundle: .module),
message: String(
localized: "Email unavailable. You can manually reach out to \(addresses.joined(separator: ", "))",
bundle: .module,
comment: "Email unavailable. Manual approach."
)
)
return
}
Expand All @@ -106,7 +110,7 @@ extension ContactOption {

private static func presentAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: String(localized: "CONTACT_MANUAL_DISMISS", bundle: .module), style: .default))
alert.addAction(UIAlertAction(title: String(localized: "Ok", bundle: .module, comment: "Dismiss alert"), style: .default))
rootViewController?.present(alert, animated: true, completion: nil)
}
}

0 comments on commit bd38bd7

Please sign in to comment.