Skip to content
Open
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 6.4
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ For users who previously used AboutKit for showing the features list, please use
2. [Integration](#integration)
3. [Usage](#usage)
- [AKConfiguration](#akconfiguration)
- [AKSection](#aksection)
- [AKEntry](#akentry)
- [AKMyApp](#akmyapp)
- [AKDeveloper](#akdeveloper)
- [AKProfile](#akprofile)
Expand Down Expand Up @@ -78,6 +80,82 @@ let configuration = AKConfiguration(
)
```

The default section order matches the standard AboutKit layout. Supply a custom `sections` array to reorder built-in sections or insert your own sections.

### AKSection

`AKSection` controls the sections and their order in the AboutKit form. The standard order is available as `AKSection.defaultOrder`. A custom section can contain one or more `AKEntry` values:

```swift
let sections: [AKSection] = [
.header,
.contact,
.custom(
title: "Purchases",
[purchaseEntry, tipJarEntry]
),
.developerProfiles,
.appProfiles,
.sharingAndReview,
.legalAndAcknowledgements,
.testFlight,
.otherApps
]
```

Omit `title` for an untitled custom section: `.custom([purchaseEntry])`.

Pass the order to `AKConfiguration` using its `sections` parameter.

### AKEntry

`AKEntry` creates a row with a title and SF Symbol that presents any SwiftUI destination using a navigation push or sheet. Both are available on iOS, watchOS, tvOS and visionOS; see [Platform differences](#platform-differences) for macOS:

```swift
let purchaseEntry = AKEntry.navigation(
"Purchase Options",
systemImage: "cart"
) {
PurchaseOptionsView()
}

let tipJarEntry = AKEntry.sheet(
"Tip Jar",
systemImage: "heart"
) {
TipJarView()
}
```

Sheet destinations are responsible for providing any navigation container or dismissal controls they require.

#### Platform differences

On macOS the form has no navigation stack to push onto, so `.navigation` and `.sheet` behave identically: both present their destination in a sheet. Rows also match the surrounding macOS style, showing the title alongside a trailing action button rather than an SF Symbol, so `systemImage` is ignored there.

This means every destination needs its own dismissal control on macOS, including those created with `.navigation`. Sheets are given a minimum size of 400×300, matching the built-in acknowledgements sheet; a destination that sets its own frame overrides this.

```swift
struct PurchaseOptionsView: View {
@Environment(\.dismiss) private var dismiss

var body: some View {
#if os(macOS)
NavigationStack {
content
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Done") { dismiss() }
}
}
}
#else
content
#endif
}
}
```

### AKMyApp

This is a struct containing details about the current app. It can be created like so:
Expand Down
49 changes: 41 additions & 8 deletions Sources/AboutKit/AboutAppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,29 @@ public struct AboutAppView: View {

public var body: some View {
Form {
ForEach(Array(configuration.sections.enumerated()), id: \.offset) { _, section in
content(for: section)
}
}
.navigationTitle(LocalizedStrings.aboutApp)
.sheet(isPresented: $showingMailSheet) {
MailView(app: configuration.app, debugDetails: AboutKit.debugDetails)
.edgesIgnoringSafeArea(.all)
}
}


// MARK: - Sections

@ViewBuilder
private func content(for section: AKSection) -> some View {
switch section {
case .header:
Section {
HeaderView(app: configuration.app)
}

case .contact:
if configuration.app.email != nil || configuration.app.websiteURL != nil {
Section {
Button(action: sendMail) {
Expand All @@ -39,7 +58,7 @@ public struct AboutAppView: View {
systemImage: "envelope"
)
}

if let websiteURL = configuration.app.websiteURL {
Button {
openURL(websiteURL)
Expand All @@ -53,6 +72,7 @@ public struct AboutAppView: View {
}
}

case .developerProfiles:
if configuration.app.developer.profiles.isEmpty == false {
Section {
ForEach(
Expand All @@ -71,6 +91,7 @@ public struct AboutAppView: View {
}
}

case .appProfiles:
if configuration.app.profiles.isEmpty == false {
Section {
ForEach(
Expand All @@ -88,7 +109,8 @@ public struct AboutAppView: View {
}
}
}


case .sharingAndReview:
if configuration.showShareApp.isVisible || configuration.showWriteReview.isVisible {
Section {
if configuration.showShareApp.isVisible {
Expand Down Expand Up @@ -116,6 +138,7 @@ public struct AboutAppView: View {
}
}

case .legalAndAcknowledgements:
if configuration.app.privacyPolicyURL != nil || configuration.app.termsOfUseURL != nil || configuration.app.acknowledgements?.frameworks?.isEmpty == false || configuration.app.acknowledgements?.people?.isEmpty == false {
Section {
if let privacyPolicyURL = configuration.app.privacyPolicyURL {
Expand Down Expand Up @@ -155,6 +178,7 @@ public struct AboutAppView: View {
}
}

case .testFlight:
if let testFlightURL = configuration.app.testFlightURL {
Section {
Button {
Expand All @@ -168,6 +192,7 @@ public struct AboutAppView: View {
}
}

case .otherApps:
if configuration.otherApps.isEmpty == false {
Section {
ForEach(configuration.otherApps, content: OtherAppRowView.init)
Expand All @@ -177,16 +202,24 @@ public struct AboutAppView: View {
} label: {
Text(LocalizedStrings.viewAllApps)
}

} header: {
Text(LocalizedStrings.otherApps)
}
}
}
.navigationTitle(LocalizedStrings.aboutApp)
.sheet(isPresented: $showingMailSheet) {
MailView(app: configuration.app, debugDetails: AboutKit.debugDetails)
.edgesIgnoringSafeArea(.all)

case .custom(let title, let entries):
if entries.isEmpty == false {
Section {
ForEach(Array(entries.enumerated()), id: \.offset) { _, entry in
entry
}
} header: {
if let title {
Text(title)
}
}
}
}
}

Expand Down
47 changes: 40 additions & 7 deletions Sources/AboutKit/MacAboutAppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,40 @@ public struct AboutAppView: View {

public var body: some View {
Form {
ForEach(Array(configuration.sections.enumerated()), id: \.offset) { _, section in
content(for: section)
}
}
.formStyle(.grouped)
.navigationTitle(LocalizedStrings.aboutApp)
.sheet(isPresented: $showingAcknowledgements) {
if let acknowledgements = configuration.app.acknowledgements {
AcknowledgementsView(acknowledgements)
}
}
}


// MARK: - Sections

@ViewBuilder
private func content(for section: AKSection) -> some View {
switch section {
case .header:
Section {
HeaderView(app: configuration.app)
.padding(.vertical, 8)
}

case .contact:
if configuration.app.email != nil || configuration.app.websiteURL != nil {
Section {
ItemLabel(
LocalizedStrings.email,
actionTitle: LocalizedStrings.contactDeveloper,
action: sendMail
)

if let websiteURL = configuration.app.websiteURL {
ItemLabel(
LocalizedStrings.website,
Expand All @@ -50,6 +71,7 @@ public struct AboutAppView: View {
}
}

case .developerProfiles:
if configuration.app.developer.profiles.isEmpty == false {
Section {
ForEach(
Expand All @@ -66,6 +88,7 @@ public struct AboutAppView: View {
}
}

case .appProfiles:
if configuration.app.profiles.isEmpty == false {
Section {
ForEach(
Expand All @@ -82,6 +105,7 @@ public struct AboutAppView: View {
}
}

case .sharingAndReview:
if configuration.showShareApp.isVisible || configuration.showWriteReview.isVisible {
Section {
if configuration.showShareApp.isVisible {
Expand Down Expand Up @@ -109,6 +133,7 @@ public struct AboutAppView: View {
}
}

case .legalAndAcknowledgements:
if configuration.app.privacyPolicyURL != nil || configuration.app.termsOfUseURL != nil || configuration.app.acknowledgements?.frameworks?.isEmpty == false || configuration.app.acknowledgements?.people?.isEmpty == false {
Section {
if let privacyPolicyURL = configuration.app.privacyPolicyURL {
Expand Down Expand Up @@ -142,6 +167,7 @@ public struct AboutAppView: View {
}
}

case .testFlight:
if let testFlightURL = configuration.app.testFlightURL {
Section {
ItemLabel(
Expand All @@ -153,6 +179,7 @@ public struct AboutAppView: View {
}
}

case .otherApps:
if configuration.otherApps.isEmpty == false {
Section {
ForEach(configuration.otherApps, content: OtherAppRowView.init)
Expand All @@ -168,12 +195,18 @@ public struct AboutAppView: View {
Text(LocalizedStrings.otherApps)
}
}
}
.formStyle(.grouped)
.navigationTitle(LocalizedStrings.aboutApp)
.sheet(isPresented: $showingAcknowledgements) {
if let acknowledgements = configuration.app.acknowledgements {
AcknowledgementsView(acknowledgements)

case .custom(let title, let entries):
if entries.isEmpty == false {
Section {
ForEach(Array(entries.enumerated()), id: \.offset) { _, entry in
entry
}
} header: {
if let title {
Text(title)
}
}
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions Sources/AboutKit/Models/AKConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// A custom struct containing details for AboutKit.
public struct AKConfiguration: Sendable {
public struct AKConfiguration {

/// A custom struct of type `AKMyApp` containing details about the current app.
public let app: AKMyApp
Expand All @@ -22,26 +22,33 @@ public struct AKConfiguration: Sendable {
/// Indicates whether to show the Write Review option.
public let showWriteReview: AKShowOption

/// The ordered sections displayed in the AboutKit form.
public let sections: [AKSection]

/// Initializes a new `AKConfiguration` struct which contains details about the
/// current app and other apps.
/// - Parameters:
/// - app: A custom struct of type `AKMyApp` containing details about the current app.
/// - otherApps: An array of `AKOtherApp` that contains details about other apps the developer owns.
/// - showShareApp: Indicates whether to show the Share App option. Defaults to `.always`.
/// - showWriteReview: Indicates whether to show the Write Review option. Defaults to `.always`.
/// - sections: The ordered sections displayed in the AboutKit form. Defaults to `AKSection.defaultOrder`.
public init(
app: AKMyApp,
otherApps: [AKOtherApp],
showShareApp: AKShowOption = .always,
showWriteReview: AKShowOption = .always
showWriteReview: AKShowOption = .always,
sections: [AKSection] = AKSection.defaultOrder
) {
self.app = app
self.otherApps = otherApps
self.showShareApp = showShareApp
self.showWriteReview = showWriteReview
self.sections = sections
}

/// An example `AKConfiguration` to be used in SwiftUI previews.
@MainActor
static let example = AKConfiguration(
app: .example,
otherApps: [.example],
Expand Down
Loading