├── .gitignore ├── Assets └── feature-graphic.png ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── AboutKit │ ├── AboutAppView.swift │ ├── Extensions │ ├── Bundle+User.swift │ ├── Bundle+Version.swift │ └── UIDevice+DeviceType.swift │ ├── MacAboutAppView.swift │ ├── Models │ ├── AKAcknowledgements.swift │ ├── AKConfiguration.swift │ ├── AKDeveloper.swift │ ├── AKFrameworkAcknowledgement.swift │ ├── AKFrameworkAcknowledgementLink.swift │ ├── AKMyApp.swift │ ├── AKOtherApp.swift │ ├── AKPersonAcknowledgement.swift │ ├── AKProfile.swift │ ├── AKProfileDisplayMode.swift │ ├── AKProfilePlatform.swift │ ├── AKShowOption.swift │ ├── AboutKit.swift │ ├── LinkedInProfileType.swift │ ├── PlatformImage.swift │ ├── RemoteImageLoadState.swift │ └── UserType.swift │ ├── Networking │ └── AppIconNetworkManager.swift │ ├── Protocols │ └── AKApp.swift │ ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── bluesky.symbolset │ │ │ ├── Contents.json │ │ │ └── bluesky.svg │ │ ├── facebook.symbolset │ │ │ ├── Contents.json │ │ │ └── facebook.svg │ │ ├── instagram.symbolset │ │ │ ├── Contents.json │ │ │ └── instagram.svg │ │ ├── linkedin.symbolset │ │ │ ├── Contents.json │ │ │ └── linkedin.svg │ │ ├── mastodon.symbolset │ │ │ ├── Contents.json │ │ │ └── mastodon.svg │ │ ├── pinterest.symbolset │ │ │ ├── Contents.json │ │ │ └── pinterest.svg │ │ ├── reddit.symbolset │ │ │ ├── Contents.json │ │ │ └── reddit.svg │ │ ├── snapchat.symbolset │ │ │ ├── Contents.json │ │ │ └── snapchat.svg │ │ ├── threads.symbolset │ │ │ ├── Contents.json │ │ │ └── threads.svg │ │ ├── tiktok.symbolset │ │ │ ├── Contents.json │ │ │ └── tiktok.svg │ │ ├── twitter.symbolset │ │ │ ├── Contents.json │ │ │ └── twitter.svg │ │ └── x.symbolset │ │ │ ├── Contents.json │ │ │ └── x.svg │ ├── Localization │ │ └── Localizable.xcstrings │ └── PrivacyInfo.xcprivacy │ ├── UIKit Views │ ├── MailView.swift │ └── ShareSheetView.swift │ ├── Utilities │ ├── LocalizedStrings.swift │ └── RemoteImageLoader.swift │ ├── Views │ ├── AcknowledgementsView.swift │ ├── FrameworkAcknowledgementView.swift │ ├── HeaderView.swift │ ├── ItemLabel.swift │ ├── MacItemLabel.swift │ ├── MacOtherAppRow.swift │ ├── OtherAppRowView.swift │ ├── PersonAcknowledgmentView.swift │ ├── RemoteImageView.swift │ ├── SectionHeaderLabel.swift │ ├── TVItemLabel.swift │ ├── TVOtherAppRowView.swift │ ├── WatchItemLabel.swift │ └── WatchOtherAppRowView.swift │ └── WatchTVAboutAppView.swift └── Tests ├── AboutKitTests ├── AboutKitTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,xcode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### OSX ### 35 | # General 36 | 37 | # Icon must end with two \r 38 | 39 | # Thumbnails 40 | 41 | # Files that might appear in the root of a volume 42 | 43 | # Directories potentially created on remote AFP share 44 | 45 | 46 | ### Swift ### 47 | # Xcode 48 | # 49 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 50 | 51 | ## User settings 52 | xcuserdata/ 53 | *.xcuserdata 54 | *.xcuserstate 55 | 56 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 57 | *.xcscmblueprint 58 | *.xccheckout 59 | 60 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 61 | build/ 62 | DerivedData/ 63 | *.moved-aside 64 | *.pbxuser 65 | !default.pbxuser 66 | *.mode1v3 67 | !default.mode1v3 68 | *.mode2v3 69 | !default.mode2v3 70 | *.perspectivev3 71 | !default.perspectivev3 72 | 73 | ## Obj-C/Swift specific 74 | *.hmap 75 | 76 | ## App packaging 77 | *.ipa 78 | *.dSYM.zip 79 | *.dSYM 80 | 81 | ## Playgrounds 82 | timeline.xctimeline 83 | playground.xcworkspace 84 | 85 | # Swift Package Manager 86 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 87 | Packages/ 88 | Package.pins 89 | Package.resolved 90 | *.xcodeproj 91 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 92 | # hence it is not needed unless you have added a package configuration file to your project 93 | .swiftpm 94 | 95 | .build/ 96 | 97 | # CocoaPods 98 | # We recommend against adding the Pods directory to your .gitignore. However 99 | # you should judge for yourself, the pros and cons are mentioned at: 100 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 101 | # Pods/ 102 | # Add this line if you want to avoid checking in source code from the Xcode workspace 103 | # *.xcworkspace 104 | 105 | # Carthage 106 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 107 | # Carthage/Checkouts 108 | 109 | Carthage/Build/ 110 | 111 | # Add this lines if you are using Accio dependency management (Deprecated since Xcode 12) 112 | # Dependencies/ 113 | # .accio/ 114 | 115 | # fastlane 116 | # It is recommended to not store the screenshots in the git repo. 117 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 118 | # For more information about the recommended setup visit: 119 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 120 | 121 | fastlane/report.xml 122 | fastlane/Preview.html 123 | fastlane/screenshots/**/*.png 124 | fastlane/test_output 125 | 126 | # Code Injection 127 | # After new code Injection tools there's a generated folder /iOSInjectionProject 128 | # https://github.com/johnno1962/injectionforxcode 129 | 130 | iOSInjectionProject/ 131 | 132 | ### SwiftPM ### 133 | Packages 134 | xcuserdata 135 | *.xcodeproj 136 | 137 | 138 | ### Xcode ### 139 | # Xcode 140 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 141 | 142 | 143 | ## Gcc Patch 144 | /*.gcno 145 | 146 | ### Xcode Patch ### 147 | *.xcodeproj/* 148 | !*.xcodeproj/project.pbxproj 149 | !*.xcodeproj/xcshareddata/ 150 | !*.xcworkspace/contents.xcworkspacedata 151 | **/xcshareddata/WorkspaceSettings.xcsettings 152 | 153 | # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode -------------------------------------------------------------------------------- /Assets/feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamfootdev/AboutKit/4428969a5a71158085f3a96767764c90f064ed1f/Assets/feature-graphic.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adam Foot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AboutKit", 8 | defaultLocalization: "en", 9 | platforms: [ 10 | .iOS(.v15), 11 | .macOS(.v13), 12 | .tvOS(.v15), 13 | .visionOS(.v1), 14 | .watchOS(.v8) 15 | ], 16 | products: [ 17 | // Products define the executables and libraries a package produces, and make them visible to other packages. 18 | .library( 19 | name: "AboutKit", 20 | targets: ["AboutKit"] 21 | ), 22 | ], 23 | dependencies: [ 24 | // Dependencies declare other packages that this package depends on. 25 | // .package(url: /* package url */, from: "1.0.0"), 26 | ], 27 | targets: [ 28 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 29 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 30 | .target( 31 | name: "AboutKit", 32 | dependencies: [], 33 | resources: [.process("Resources")], 34 | swiftSettings: [ 35 | .swiftLanguageMode(.v6) 36 | ] 37 | ), 38 | .testTarget( 39 | name: "AboutKitTests", 40 | dependencies: ["AboutKit"] 41 | ), 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AboutKit 2 | 3 | ![Feature Graphic](https://github.com/adamfootdev/AboutKit/blob/main/Assets/feature-graphic.png?raw=true) 4 | ![Platform](https://img.shields.io/badge/platforms-iOS%2FiPadOS%2015.0%2B%20%7C%20macOS%2013.0%2B%20%7C%20tvOS%2015.0%2B%20%7C%20visionOS%201.0%2B%20%7C%20watchOS%208.0%2B-blue) 5 | 6 | AboutKit provides developers for Apple platforms with the ability to add an About screen to their apps. This is built using SwiftUI so can be displayed natively from a SwiftUI app or using a UIHostingController in a UIKit app. 7 | 8 | This has been localised into multiple languages and the buttons will automatically adapt to your appʼs accent color. 9 | 10 | For users who previously used AboutKit for showing the features list, please use [FeaturesKit](https://github.com/adamfootdev/FeaturesKit). 11 | 12 | 1. [Requirements](#requirements) 13 | 2. [Integration](#integration) 14 | 3. [Usage](#usage) 15 | - [AKConfiguration](#akconfiguration) 16 | - [AKMyApp](#akmyapp) 17 | - [AKDeveloper](#akdeveloper) 18 | - [AKProfile](#akprofile) 19 | - [AKOtherApp](#akotherapp) 20 | - [AKAcknowledgements](#akacknowledgements) 21 | - [AKPersonAcknowledgement](#akpersonacknowledgement) 22 | - [AKFrameworkAcknowledgement](#akframeworkacknowledgement) 23 | - [AboutAppView](#aboutappview) 24 | 4. [Other Packages](#other-packages) 25 | - [FeaturesKit](https://github.com/adamfootdev/FeaturesKit) 26 | - [HelpKit](https://github.com/adamfootdev/HelpKit) 27 | - [HapticsKit](https://github.com/adamfootdev/HapticsKit) 28 | 29 | ## Requirements 30 | 31 | - iOS/iPadOS 15.0+ 32 | - macOS 13.0+ 33 | - tvOS 15.0+ 34 | - visionOS 1.0+ 35 | - watchOS 8.0+ 36 | - Xcode 15.0+ 37 | 38 | ## Integration 39 | 40 | ### Swift Package Manager 41 | 42 | AboutKit can be added to your app via Swift Package Manager in Xcode. Add to your project like so: 43 | 44 | ```swift 45 | dependencies: [ 46 | .package(url: "https://github.com/adamfootdev/AboutKit.git", from: "3.0.0") 47 | ] 48 | ``` 49 | 50 | ## Usage 51 | 52 | To start using the framework, you'll need to import it first: 53 | 54 | ```swift 55 | import AboutKit 56 | ``` 57 | 58 | ### AKConfiguration 59 | 60 | This is a struct containing all of the relevant details required to configure AboutKit. It can be created like so: 61 | 62 | ```swift 63 | let configuration = AKConfiguration( 64 | app: app, 65 | otherApps: otherApps, 66 | showShareApp: .always, 67 | showWriteReview: .always 68 | ) 69 | ``` 70 | 71 | ### AKMyApp 72 | 73 | This is a struct containing details about the current app. It can be created like so: 74 | 75 | ```swift 76 | let app = AKMyApp( 77 | id: "123456789", 78 | name: "Example App", 79 | appIcon: UIImage(named: "app-icon"), 80 | developer: developer, 81 | email: "exampleapp@example.com", 82 | websiteURL: URL(string: "https://www.example.com")!, 83 | profiles: [profile], 84 | privacyPolicyURL: URL(string: "https://www.example.com/privacy-policy")!, 85 | termsOfUseURL: URL(string: "https://www.example.com/terms-of-use")!, 86 | testFlightURL: URL(string: "https://www.example.com/testflight")!, 87 | acknowledgements: acknowledgements 88 | ) 89 | ``` 90 | 91 | If a value for the app icon is not provided, one will attempt to be downloaded from the App Store based on the provided app ID. The app ID can be found in App Store Connect or from the app's URL, e.g. 92 | 93 | ### AKDeveloper 94 | 95 | This is a struct containing details about the developer belonging to the current app. It can be created like so: 96 | 97 | ```swift 98 | let developer = AKDeveloper( 99 | id: "987654321", 100 | name: "App Developer", 101 | profiles: [profile] 102 | ) 103 | ``` 104 | 105 | The developer ID can be found by locating the App Store page that contains all of your apps e.g. 106 | 107 | ### AKProfile 108 | 109 | This is a struct containing details about about a social media profile relating to either the developer or the app itself. It supports multiple platforms such as X and Mastodon. It can be created like so: 110 | 111 | ```swift 112 | let profile = AKProfile( 113 | username: "appdeveloper", 114 | platform: .reddit 115 | ) 116 | ``` 117 | 118 | ### AKOtherApp 119 | 120 | This is a struct which contains details to display another app that you own and want to show in a list on the about screen. You can create one as follows: 121 | 122 | ```swift 123 | let otherApp = AKOtherApp( 124 | id: "543216789", 125 | name: "Other App", 126 | appIcon: UIImage(named: "app-icon") 127 | ) 128 | ``` 129 | 130 | If a value for the app icon is not provided, one will attempt to be downloaded from the App Store based on the provided app ID. The app ID can be found in App Store Connect or from the app's URL, e.g. 131 | 132 | ### AKAcknowledgements 133 | 134 | This is a struct which contains details about frameworks and people youʼd like to acknowledge. You can create one as follows: 135 | 136 | ```swift 137 | let acknowledgements = AKAcknowledgements( 138 | people: [person], 139 | frameworks: [framework] 140 | ) 141 | ``` 142 | 143 | ### AKPersonAcknowledgement 144 | 145 | This is a struct which contains details about a person youʼd like to acknowledge. You can create one as follows: 146 | 147 | ```swift 148 | let person = AKPersonAcknowledgement( 149 | name: "App Developer", 150 | details: "Some details about this person!", 151 | profiles: [profile] 152 | ) 153 | ``` 154 | 155 | ### AKFrameworkAcknowledgement 156 | 157 | This is a struct which contains details about a framework youʼd like to acknowledge. You can create one as follows: 158 | 159 | ```swift 160 | let framework = AKFrameworkAcknowledgement( 161 | name: "Framework", 162 | details: "Some details about this framework!", 163 | links: [.productPage(URL(string: "https://www.example.com")!)] 164 | ) 165 | ``` 166 | 167 | ### AboutAppView 168 | 169 | Create an instance of the view using the following: 170 | 171 | ```swift 172 | AboutAppView(configuration: configuration) 173 | ``` 174 | 175 | ## Other Packages 176 | 177 | ### [FeaturesKit](https://github.com/adamfootdev/FeaturesKit) 178 | 179 | Add a features list screen to your app. 180 | 181 | ### [HelpKit](https://github.com/adamfootdev/HelpKit) 182 | 183 | Add a help screen to your app. 184 | 185 | ### [HapticsKit](https://github.com/adamfootdev/HapticsKit) 186 | 187 | Add haptic feedback to your app. 188 | -------------------------------------------------------------------------------- /Sources/AboutKit/AboutAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutAppView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | #if os(iOS) || os(visionOS) 9 | import SwiftUI 10 | import MessageUI 11 | 12 | /// A SwiftUI `View` which displays attributes and links relating to an app. 13 | public struct AboutAppView: View { 14 | @Environment(\.openURL) var openURL 15 | 16 | /// A custom struct of type `AKConfiguration` containing details for AboutKit. 17 | private let configuration: AKConfiguration 18 | 19 | /// A `Bool` indicating whether the Mail sheet is currently showing. 20 | @State private var showingMailSheet: Bool = false 21 | 22 | /// A `Bool` indicating whether the share sheet is currently showing. 23 | @State private var showingShareSheet: Bool = false 24 | 25 | /// Initializes a new SwiftUI `View` which displays attributes and links relating to an app. 26 | /// - Parameter configuration: A custom struct of type `AKConfiguration` containing details for AboutKit. 27 | public init(configuration: AKConfiguration) { 28 | self.configuration = configuration 29 | } 30 | 31 | public var body: some View { 32 | Form { 33 | Section { 34 | HeaderView(app: configuration.app) 35 | } 36 | 37 | if configuration.app.email != nil || configuration.app.websiteURL != nil { 38 | Section { 39 | Button(action: sendMail) { 40 | ItemLabel( 41 | LocalizedStrings.email, 42 | systemImage: "envelope" 43 | ) 44 | } 45 | 46 | if let websiteURL = configuration.app.websiteURL { 47 | Button { 48 | openURL(websiteURL) 49 | } label: { 50 | ItemLabel( 51 | LocalizedStrings.website, 52 | systemImage: "safari" 53 | ) 54 | } 55 | } 56 | } 57 | } 58 | 59 | if configuration.app.developer.profiles.isEmpty == false { 60 | Section { 61 | ForEach( 62 | Array(configuration.app.developer.profiles.enumerated()), 63 | id: \.0 64 | ) { _, profile in 65 | Button { 66 | openURL(profile.url) 67 | } label: { 68 | ItemLabel( 69 | profile.title, 70 | image: profile.platform.imageName 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | 77 | if configuration.app.profiles.isEmpty == false { 78 | Section { 79 | ForEach( 80 | Array(configuration.app.profiles.enumerated()), 81 | id: \.0 82 | ) { _, profile in 83 | Button { 84 | openURL(profile.url) 85 | } label: { 86 | ItemLabel( 87 | profile.title, 88 | image: profile.platform.imageName 89 | ) 90 | } 91 | } 92 | } 93 | } 94 | 95 | if configuration.showShareApp.isVisible || configuration.showWriteReview.isVisible { 96 | Section { 97 | if configuration.showShareApp.isVisible { 98 | if #available(iOS 16.0, *) { 99 | ShareLink( 100 | item: configuration.app.appStoreShareURL, 101 | message: Text(String(localized: "Check out \(configuration.app.name) on the App Store!", bundle: .module)) 102 | ) { 103 | ItemLabel( 104 | LocalizedStrings.shareApp, 105 | systemImage: "square.and.arrow.up" 106 | ) 107 | } 108 | } else { 109 | Button { 110 | showingShareSheet = true 111 | } label: { 112 | ItemLabel( 113 | LocalizedStrings.shareApp, 114 | systemImage: "square.and.arrow.up" 115 | ) 116 | } 117 | } 118 | } 119 | 120 | if configuration.showWriteReview.isVisible { 121 | Button { 122 | openURL(configuration.app.appStoreReviewURL) 123 | } label: { 124 | ItemLabel( 125 | LocalizedStrings.writeReview, 126 | systemImage: "star" 127 | ) 128 | } 129 | } 130 | } 131 | } 132 | 133 | if configuration.app.privacyPolicyURL != nil || configuration.app.termsOfUseURL != nil || configuration.app.acknowledgements?.frameworks?.isEmpty == false || configuration.app.acknowledgements?.people?.isEmpty == false { 134 | Section { 135 | if let privacyPolicyURL = configuration.app.privacyPolicyURL { 136 | Button { 137 | openURL(privacyPolicyURL) 138 | } label: { 139 | ItemLabel( 140 | LocalizedStrings.privacyPolicy, 141 | systemImage: "lock.shield" 142 | ) 143 | } 144 | } 145 | 146 | if let termsOfUseURL = configuration.app.termsOfUseURL { 147 | Button { 148 | openURL(termsOfUseURL) 149 | } label: { 150 | ItemLabel( 151 | LocalizedStrings.termsOfUse, 152 | systemImage: "checkmark.seal" 153 | ) 154 | } 155 | } 156 | 157 | if let acknowledgements = configuration.app.acknowledgements { 158 | if acknowledgements.frameworks?.isEmpty == false || acknowledgements.people?.isEmpty == false { 159 | NavigationLink { 160 | AcknowledgementsView(acknowledgements) 161 | } label: { 162 | ItemLabel( 163 | LocalizedStrings.acknowledgements, 164 | systemImage: "list.star" 165 | ) 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | if let testFlightURL = configuration.app.testFlightURL { 173 | Section { 174 | Button { 175 | openURL(testFlightURL) 176 | } label: { 177 | ItemLabel( 178 | LocalizedStrings.testFlight, 179 | systemImage: "fan" 180 | ) 181 | } 182 | } 183 | } 184 | 185 | if configuration.otherApps.isEmpty == false { 186 | Section { 187 | ForEach(configuration.otherApps, content: OtherAppRowView.init) 188 | 189 | Button { 190 | openURL(configuration.app.developer.appStoreURL) 191 | } label: { 192 | Text(LocalizedStrings.viewAllApps) 193 | } 194 | 195 | } header: { 196 | Text(LocalizedStrings.otherApps) 197 | } 198 | } 199 | } 200 | .navigationTitle(LocalizedStrings.aboutApp) 201 | .sheet(isPresented: $showingMailSheet) { 202 | MailView(app: configuration.app, debugDetails: AboutKit.debugDetails) 203 | .edgesIgnoringSafeArea(.all) 204 | } 205 | .sheet(isPresented: $showingShareSheet) { 206 | ShareSheetView(app: configuration.app) 207 | .edgesIgnoringSafeArea(.all) 208 | } 209 | } 210 | 211 | 212 | // MARK: - Mail 213 | 214 | private func sendMail() { 215 | if MFMailComposeViewController.canSendMail() { 216 | showingMailSheet = true 217 | } else { 218 | guard let subject = configuration.app.name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 219 | let body = AboutKit.debugDetails.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return } 220 | 221 | guard let email = configuration.app.email else { return } 222 | 223 | let urlString = "mailto:\(email)?subject=\(subject)%20-%20Support&body=\(body)" 224 | 225 | if let url = URL(string: urlString) { 226 | openURL(url) 227 | } 228 | } 229 | } 230 | } 231 | 232 | struct AboutAppView_Previews: PreviewProvider { 233 | static var previews: some View { 234 | NavigationView { 235 | AboutAppView(configuration: .example) 236 | } 237 | .navigationViewStyle(.stack) 238 | } 239 | } 240 | #endif 241 | -------------------------------------------------------------------------------- /Sources/AboutKit/Extensions/Bundle+User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+User.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 02/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | 12 | /// Returns the `UserType` depending on the current environment that the user is using. 13 | var userType: UserType { 14 | #if DEBUG 15 | return .debug 16 | #else 17 | if appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { 18 | return .testFlight 19 | } else { 20 | return .appStore 21 | } 22 | #endif 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AboutKit/Extensions/Bundle+Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Version.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 02/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | 12 | /// Returns a `String` with the current app version number, e.g. 1.0. 13 | var versionNumber: String { 14 | infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 15 | } 16 | 17 | /// Returns a `String` with the current app build number, e.g. 1. 18 | var buildNumber: String { 19 | infoDictionary?["CFBundleVersion"] as? String ?? "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AboutKit/Extensions/UIDevice+DeviceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+DeviceType.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 02/08/2023. 6 | // 7 | 8 | #if os(iOS) || os(visionOS) 9 | import UIKit 10 | 11 | extension UIDevice { 12 | 13 | /// Returns a `String` containing the identifier of the current device, e.g. iPhone 13,1 14 | var deviceType: String { 15 | var systemInfo = utsname() 16 | uname(&systemInfo) 17 | let machineMirror = Mirror(reflecting: systemInfo.machine) 18 | 19 | let identifier = machineMirror.children.reduce("") { identifier, element in 20 | guard let value = element.value as? Int8, 21 | value != 0 else { 22 | return identifier 23 | } 24 | 25 | return identifier + String(UnicodeScalar(UInt8(value))) 26 | } 27 | 28 | return identifier 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/AboutKit/MacAboutAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacAboutAppView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/03/2023. 6 | // 7 | 8 | #if os(macOS) 9 | import SwiftUI 10 | 11 | /// A SwiftUI `View` which displays attributes and links relating to an app. 12 | public struct AboutAppView: View { 13 | @Environment(\.openURL) var openURL 14 | 15 | /// A custom struct of type `AKConfiguration` containing details for AboutKit. 16 | private let configuration: AKConfiguration 17 | 18 | /// A `Boolean` indicating whether the Acknowledgements view is visible. 19 | @State private var showingAcknowledgements: Bool = false 20 | 21 | /// Initializes a new SwiftUI `View` which displays attributes and links relating to an app. 22 | /// - Parameter configuration: A custom struct of type `AKConfiguration` containing details for AboutKit. 23 | public init(configuration: AKConfiguration) { 24 | self.configuration = configuration 25 | } 26 | 27 | public var body: some View { 28 | Form { 29 | Section { 30 | HeaderView(app: configuration.app) 31 | .padding(.vertical, 8) 32 | } 33 | 34 | if configuration.app.email != nil || configuration.app.websiteURL != nil { 35 | Section { 36 | ItemLabel( 37 | LocalizedStrings.email, 38 | actionTitle: LocalizedStrings.contactDeveloper, 39 | action: sendMail 40 | ) 41 | 42 | if let websiteURL = configuration.app.websiteURL { 43 | ItemLabel( 44 | LocalizedStrings.website, 45 | actionTitle: LocalizedStrings.openWebsite 46 | ) { 47 | openURL(websiteURL) 48 | } 49 | } 50 | } 51 | } 52 | 53 | if configuration.app.developer.profiles.isEmpty == false { 54 | Section { 55 | ForEach( 56 | Array(configuration.app.developer.profiles.enumerated()), 57 | id: \.1 58 | ) { _, profile in 59 | ItemLabel( 60 | profile.title, 61 | actionTitle: LocalizedStrings.viewProfile 62 | ) { 63 | openURL(profile.url) 64 | } 65 | } 66 | } 67 | } 68 | 69 | if configuration.app.profiles.isEmpty == false { 70 | Section { 71 | ForEach( 72 | Array(configuration.app.profiles.enumerated()), 73 | id: \.1 74 | ) { _, profile in 75 | ItemLabel( 76 | profile.title, 77 | actionTitle: LocalizedStrings.viewProfile 78 | ) { 79 | openURL(profile.url) 80 | } 81 | } 82 | } 83 | } 84 | 85 | if configuration.showShareApp.isVisible || configuration.showWriteReview.isVisible { 86 | Section { 87 | if configuration.showShareApp.isVisible { 88 | HStack { 89 | Text(LocalizedStrings.shareApp) 90 | Spacer() 91 | 92 | ShareLink( 93 | item: configuration.app.appStoreShareURL, 94 | message: Text(String(localized: "Check out \(configuration.app.name) on the App Store!", bundle: .module)) 95 | ) { 96 | Text(LocalizedStrings.share) 97 | } 98 | } 99 | } 100 | 101 | if configuration.showWriteReview.isVisible { 102 | ItemLabel( 103 | LocalizedStrings.writeReview, 104 | actionTitle: LocalizedStrings.review 105 | ) { 106 | openURL(configuration.app.appStoreReviewURL) 107 | } 108 | } 109 | } 110 | } 111 | 112 | if configuration.app.privacyPolicyURL != nil || configuration.app.termsOfUseURL != nil || configuration.app.acknowledgements?.frameworks?.isEmpty == false || configuration.app.acknowledgements?.people?.isEmpty == false { 113 | Section { 114 | if let privacyPolicyURL = configuration.app.privacyPolicyURL { 115 | ItemLabel( 116 | LocalizedStrings.privacyPolicy, 117 | actionTitle: LocalizedStrings.viewPrivacyPolicy 118 | ) { 119 | openURL(privacyPolicyURL) 120 | } 121 | } 122 | 123 | if let termsOfUseURL = configuration.app.termsOfUseURL { 124 | ItemLabel( 125 | LocalizedStrings.termsOfUse, 126 | actionTitle: LocalizedStrings.viewTermsOfUse 127 | ) { 128 | openURL(termsOfUseURL) 129 | } 130 | } 131 | 132 | if let acknowledgements = configuration.app.acknowledgements { 133 | if acknowledgements.frameworks?.isEmpty == false || acknowledgements.people?.isEmpty == false { 134 | ItemLabel( 135 | LocalizedStrings.acknowledgements, 136 | actionTitle: LocalizedStrings.viewAcknowledgements 137 | ) { 138 | showingAcknowledgements.toggle() 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | if let testFlightURL = configuration.app.testFlightURL { 146 | Section { 147 | ItemLabel( 148 | LocalizedStrings.testFlight, 149 | actionTitle: LocalizedStrings.openTestFlight 150 | ) { 151 | openURL(testFlightURL) 152 | } 153 | } 154 | } 155 | 156 | if configuration.otherApps.isEmpty == false { 157 | Section { 158 | ForEach(configuration.otherApps, content: OtherAppRowView.init) 159 | 160 | ItemLabel( 161 | LocalizedStrings.viewAllApps, 162 | actionTitle: LocalizedStrings.viewMac 163 | ) { 164 | openURL(configuration.app.developer.appStoreURL) 165 | } 166 | 167 | } header: { 168 | Text(LocalizedStrings.otherApps) 169 | } 170 | } 171 | } 172 | .formStyle(.grouped) 173 | .navigationTitle(LocalizedStrings.aboutApp) 174 | .sheet(isPresented: $showingAcknowledgements) { 175 | if let acknowledgements = configuration.app.acknowledgements { 176 | AcknowledgementsView(acknowledgements) 177 | } 178 | } 179 | } 180 | 181 | 182 | // MARK: - Mail 183 | 184 | private func sendMail() { 185 | guard let subject = configuration.app.name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 186 | let body = AboutKit.debugDetails.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return } 187 | 188 | guard let email = configuration.app.email else { return } 189 | 190 | let urlString = "mailto:\(email)?subject=\(subject)%20-%20Support&body=\(body)" 191 | 192 | if let url = URL(string: urlString) { 193 | openURL(url) 194 | } 195 | } 196 | } 197 | 198 | struct AboutAppView_Previews: PreviewProvider { 199 | static var previews: some View { 200 | AboutAppView(configuration: .example) 201 | } 202 | } 203 | #endif 204 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKAcknowledgements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKAcknowledgements.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom struct containing details about the acknowledgements for the app. 11 | public struct AKAcknowledgements: Sendable { 12 | 13 | /// An `Optional` array of `AKPersonAcknowledgement` that contains details about the people to acknowledge in developing the app. 14 | public let people: [AKPersonAcknowledgement]? 15 | 16 | /// An `Optional` array of `AKFrameworkAcknowledgement` that contains details about the frameworks used in the app. 17 | public let frameworks: [AKFrameworkAcknowledgement]? 18 | 19 | /// Initalizes a custom struct containing details about the acknowledgements for the app. 20 | /// - Parameters: 21 | /// - people: An `Optional` array of `AKPersonAcknowledgement` that contains details about the people to acknowledge in developing the app. 22 | /// - frameworks: An `Optional` array of `AKFrameworkAcknowledgement` that contains details about the frameworks used in the app. 23 | public init( 24 | people: [AKPersonAcknowledgement]?, 25 | frameworks: [AKFrameworkAcknowledgement]? 26 | 27 | ) { 28 | self.people = people 29 | self.frameworks = frameworks 30 | } 31 | 32 | /// An example `AKAcknowledgements` to be used in SwiftUI previews. 33 | public static let example = AKAcknowledgements( 34 | people: [.example], 35 | frameworks: [.example] 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKConfiguration.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 29/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom struct containing details for AboutKit. 11 | public struct AKConfiguration: Sendable { 12 | 13 | /// A custom struct of type `AKMyApp` containing details about the current app. 14 | public let app: AKMyApp 15 | 16 | /// An array of `AKOtherApp` that contains details about other apps the developer owns. 17 | public let otherApps: [AKOtherApp] 18 | 19 | /// Indicates whether to show the Share App option. 20 | public let showShareApp: AKShowOption 21 | 22 | /// Indicates whether to show the Write Review option. 23 | public let showWriteReview: AKShowOption 24 | 25 | /// Initializes a new `AKConfiguration` struct which contains details about the 26 | /// current app and other apps. 27 | /// - Parameters: 28 | /// - app: A custom struct of type `AKMyApp` containing details about the current app. 29 | /// - otherApps: An array of `AKOtherApp` that contains details about other apps the developer owns. 30 | /// - showShareApp: Indicates whether to show the Share App option. Defaults to `.always`. 31 | /// - showWriteReview: Indicates whether to show the Write Review option. Defaults to `.always`. 32 | public init( 33 | app: AKMyApp, 34 | otherApps: [AKOtherApp], 35 | showShareApp: AKShowOption = .always, 36 | showWriteReview: AKShowOption = .always 37 | ) { 38 | self.app = app 39 | self.otherApps = otherApps 40 | self.showShareApp = showShareApp 41 | self.showWriteReview = showWriteReview 42 | } 43 | 44 | /// An example `AKConfiguration` to be used in SwiftUI previews. 45 | static let example = AKConfiguration( 46 | app: .example, 47 | otherApps: [.example], 48 | showShareApp: .always, 49 | showWriteReview: .always 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKDeveloper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKDeveloper.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom struct containing details about the developer. 11 | public struct AKDeveloper: Sendable { 12 | 13 | /// The developer ID `String` for the given app. This can be found by locating the App Store URL 14 | /// for the developer. This should be in the format 123456789. 15 | public let id: String 16 | 17 | /// A `String` containing the developerʼs name. 18 | public let name: String 19 | 20 | /// An array of `AKProfile` containing the social media pages for the developer. 21 | public let profiles: [AKProfile] 22 | 23 | /// Initializes a custom struct of data pertaining to developer of an app. 24 | /// - Parameters: 25 | /// - id: The developer ID `String` for the given app. This can be found by locating the App Store URL 26 | /// for the developer. This should be in the format 123456789. 27 | /// - name: A `String` containing the developerʼs name. 28 | /// - profiles: An `Optional` array of `AKProfile` containing the social media pages for the developer. 29 | public init(id: String, name: String, profiles: [AKProfile]?) { 30 | self.id = id 31 | self.name = name 32 | self.profiles = profiles ?? [] 33 | } 34 | 35 | /// The App Store `URL` for the developer page based on the ID. 36 | public var appStoreURL: URL { 37 | URL(string: "https://apps.apple.com/developer/id\(id)")! 38 | } 39 | 40 | /// An example `AKDeveloper` to be used in SwiftUI previews. 41 | public static let example = AKDeveloper( 42 | id: "123456789", 43 | name: "App Developer", 44 | profiles: [ 45 | .example 46 | ] 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKFrameworkAcknowledgement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKFrameworkAcknowledgement.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom struct containing details about the frameworks used in the app. 11 | public struct AKFrameworkAcknowledgement: Sendable { 12 | 13 | /// A `String` containing the frameworkʼs name. 14 | public let name: String 15 | 16 | /// An `Optional` containing details about the framework such as licence information. 17 | public let details: String? 18 | 19 | /// An `Optional` array of `AKFrameworkAcknowledgementLink` containing links related to the framework. 20 | public let links: [AKFrameworkAcknowledgementLink]? 21 | 22 | /// Initializes a custom struct containing details about the frameworks used in the app. 23 | /// - Parameters: 24 | /// - name: A `String` containing the frameworkʼs name. 25 | /// - details: An `Optional` containing details about the framework such as licence information. 26 | /// - links: An `Optional` array of `AKFrameworkAcknowledgementLink` containing links related to the framework. 27 | public init( 28 | _ name: String, 29 | details: String?, 30 | links: [AKFrameworkAcknowledgementLink]? 31 | ) { 32 | self.name = name 33 | self.details = details 34 | self.links = links 35 | } 36 | 37 | /// An example `AKFrameworkAcknowledgement` to be used in SwiftUI previews. 38 | public static let example = AKFrameworkAcknowledgement( 39 | "AboutKit", 40 | details: "Some details here…", 41 | links: [.repository(URL(string: "https://github.com/adamfootdev/AboutKit")!)] 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKFrameworkAcknowledgementLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKFrameworkAcknowledgementLink.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom enum used for framework links. 11 | public enum AKFrameworkAcknowledgementLink: Sendable { 12 | case productPage(_ url: URL) 13 | case repository(_ url: URL) 14 | case compliance(_ url: URL) 15 | case custom(_ title: String, systemImage: String = "link", url: URL) 16 | 17 | /// A `String` containing the title of the link. 18 | var title: String { 19 | switch self { 20 | case .productPage(_): 21 | return LocalizedStrings.productPage 22 | case .repository(_): 23 | return LocalizedStrings.repository 24 | case .compliance(_): 25 | return LocalizedStrings.compliance 26 | case .custom(let title, _, _): 27 | return title 28 | } 29 | } 30 | 31 | /// A `String` containing the system image to use for the link. 32 | var systemImage: String { 33 | switch self { 34 | case .productPage(_): 35 | return "house" 36 | case .repository(_): 37 | return "hammer" 38 | case .compliance(_): 39 | return "checkmark.shield" 40 | case .custom(_, let systemImage, _): 41 | return systemImage 42 | } 43 | } 44 | 45 | /// The `URL` to open when the link is selected. 46 | var url: URL { 47 | switch self { 48 | case .productPage(let url): 49 | return url 50 | case .repository(let url): 51 | return url 52 | case .compliance(let url): 53 | return url 54 | case .custom(_, _, let url): 55 | return url 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKMyApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKApp.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A custom struct of containing details about the developerʼs app. 11 | public struct AKMyApp: AKApp, Sendable { 12 | 13 | /// The app ID `String` for the given app. This can be found in App Store Connect or the URL of the App Store 14 | /// listing for the app. This should be in the format 123456789. 15 | public let id: String 16 | 17 | /// A `String` containing the appʼs name. 18 | public let name: String 19 | 20 | /// An `Optional` containing the app icon. 21 | /// If an image is not specified, the app icon will be fetched from the App Store. 22 | public let appIcon: PlatformImage? 23 | 24 | /// A custom `AKDeveloper` struct containing details about the developer of the app. 25 | public let developer: AKDeveloper 26 | 27 | /// An `Optional` containing the email address to contact the app's support. 28 | public let email: String? 29 | 30 | /// An `Optional` containing the app's website. 31 | public let websiteURL: URL? 32 | 33 | /// An array of `AKProfile` containing the social media pages for the app. 34 | public let profiles: [AKProfile] 35 | 36 | /// An `Optional` containing the privacy policy for the app. 37 | public let privacyPolicyURL: URL? 38 | 39 | /// An `Optional` containing the terms of use for the app. 40 | public let termsOfUseURL: URL? 41 | 42 | /// An `Optional` containing the link to TestFlight for the app. 43 | public let testFlightURL: URL? 44 | 45 | /// An `Optional` struct containing all the acknowledgments for the app. 46 | public let acknowledgements: AKAcknowledgements? 47 | 48 | /// Initializes a custom struct of data pertaining to the specified app. 49 | /// - Parameters: 50 | /// - id: The app ID `String` for the given app. This can be found in App Store Connect or the URL of the App Store 51 | /// listing for the app. This should be in the format 123456789. 52 | /// - name: A `String` containing the app's name. 53 | /// - appIcon: An `Optional` containing the app icon. 54 | /// If an image is not specified, the app icon will be fetched from the App Store. 55 | /// - developer: A custom `AKDeveloper` struct containing details about the developer of the app. 56 | /// - email: An `Optional` containing the email address to contact the app's support. 57 | /// - websiteURL: An `Optional` containing the app's website. 58 | /// - profiles: An `Optional` array of `AKProfile` containing the social media pages for the app. 59 | /// - privacyPolicyURL: An `Optional` containing the privacy policy for the app. 60 | /// - termsOfUseURL: An `Optional` containing the terms of use for the app. 61 | /// - testFlightURL: An `Optional` containing the link to TestFlight for the app. Defaults to `nil`. 62 | /// - acknowledgements: An `Optional` struct containing all the acknowledgments for the app. Defaults to `nil`. 63 | public init( 64 | id: String, 65 | name: String, 66 | appIcon: PlatformImage?, 67 | developer: AKDeveloper, 68 | email: String?, 69 | websiteURL: URL?, 70 | profiles: [AKProfile]?, 71 | privacyPolicyURL: URL?, 72 | termsOfUseURL: URL?, 73 | testFlightURL: URL? = nil, 74 | acknowledgements: AKAcknowledgements? = nil 75 | ) { 76 | self.id = id 77 | self.name = name 78 | self.appIcon = appIcon 79 | self.developer = developer 80 | self.email = email 81 | self.websiteURL = websiteURL 82 | self.profiles = profiles ?? [] 83 | self.privacyPolicyURL = privacyPolicyURL 84 | self.termsOfUseURL = termsOfUseURL 85 | self.testFlightURL = testFlightURL 86 | self.acknowledgements = acknowledgements 87 | } 88 | 89 | /// An example `AKMyApp` to be used in SwiftUI previews. 90 | public static let example = AKMyApp( 91 | id: "123456789", 92 | name: "Example", 93 | appIcon: nil, 94 | developer: .example, 95 | email: "exampleapp@example.com", 96 | websiteURL: URL(string: "https://www.example.com")!, 97 | profiles: [.example], 98 | privacyPolicyURL: URL(string: "https://www.example.com/privacy-policy")!, 99 | termsOfUseURL: URL(string: "https://www.example.com/terms-of-use")!, 100 | testFlightURL: URL(string: "https://www.example.com/testflight")!, 101 | acknowledgements: .example 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKOtherApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKOtherApp.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A custom struct of data to create a promoted other app list item. 11 | public struct AKOtherApp: AKApp, Sendable { 12 | 13 | /// The app ID `String` for the given app. This can be found in App Store Connect or the URL of the App Store 14 | /// listing for the app. This should be in the format 123456789. 15 | public let id: String 16 | 17 | /// A `String` containing the app's name. 18 | public let name: String 19 | 20 | /// An `Optional` containing the app icon. 21 | /// If an image is not specified, the app icon will be fetched from the App Store. 22 | public let appIcon: PlatformImage? 23 | 24 | /// Initializes a custom struct of data to create a promoted other app list item. 25 | /// - Parameters: 26 | /// - id: The app ID `String` for the given app. This can be found in App Store Connect or the URL of the App Store 27 | /// listing for the app. This should be in the format 123456789. 28 | /// This should be in the format 123456789. 29 | /// - name: A `String` containing the app's name. 30 | /// - appIcon: An `Optional` containing the app icon. 31 | /// If an image is not specified, the app icon will be fetched from the App Store. 32 | public init( 33 | id: String, 34 | name: String, 35 | appIcon: PlatformImage? = nil 36 | ) { 37 | self.id = id 38 | self.name = name 39 | self.appIcon = appIcon 40 | } 41 | 42 | /// An example `AKOtherApp` to be used in SwiftUI previews. 43 | public static let example = AKOtherApp( 44 | id: "987654321", 45 | name: "Other App" 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKPersonAcknowledgement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKPersonAcknowledgement.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom struct containing details about the people to acknowledge in developing the app. 11 | public struct AKPersonAcknowledgement: Sendable { 12 | 13 | /// A `String` containing the personʼs name. 14 | public let name: String 15 | 16 | /// An `Optional` containing details about how they influenced the app. 17 | public let details: String? 18 | 19 | /// An `Optional` array of `AKProfile` containing their social media profiles. 20 | public let profiles: [AKProfile]? 21 | 22 | /// Initializes a custom struct containing details about the people to acknowledge in developing the app. 23 | /// - Parameters: 24 | /// - name: A `String` containing the personʼs name. 25 | /// - details: An `Optional` containing details about how they influenced the app. 26 | /// - profiles: An `Optional` array of `AKProfile` containing their social media profiles. 27 | public init( 28 | _ name: String, 29 | details: String?, 30 | profiles: [AKProfile]? 31 | ) { 32 | self.name = name 33 | self.details = details 34 | self.profiles = profiles 35 | } 36 | 37 | /// An example `AKPersonAcknowledgement` to be used in SwiftUI previews. 38 | public static let example = AKPersonAcknowledgement( 39 | "John Appleseed", 40 | details: "Some details here…", 41 | profiles: [.example] 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKProfile.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 26/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom struct containing details about a social media platform profile. 11 | public struct AKProfile: Identifiable, Hashable, Sendable { 12 | 13 | /// A `String` containing the username for the profile. 14 | public let username: String 15 | 16 | /// The platform for the profile. 17 | public let platform: AKProfilePlatform 18 | 19 | /// Controls the visibility of the username or platform name. 20 | public let displayMode: AKProfileDisplayMode 21 | 22 | /// Creates a new social media platform profile. 23 | /// - Parameters: 24 | /// - username: A `String` containing the username for the profile. 25 | /// - platform: The platform for the profile. 26 | /// - displayMode: Controls the visibility of the username or platform name. Defaults to `.combined`. 27 | public init( 28 | username: String, 29 | platform: AKProfilePlatform, 30 | displayMode: AKProfileDisplayMode = .combined 31 | ) { 32 | self.username = username.replacingOccurrences(of: "@", with: "") 33 | self.platform = platform 34 | self.displayMode = displayMode 35 | } 36 | 37 | /// The ID `String` for the profile. 38 | public var id: String { 39 | "\(username)-\(platform.name)" 40 | } 41 | 42 | /// A `String` containing the display title of the profile. 43 | var title: String { 44 | switch displayMode { 45 | case .combined: 46 | return "\(platform.name) (\(displayUsername))" 47 | case .platformOnly: 48 | return platform.name 49 | case .usernameOnly: 50 | return displayUsername 51 | case .custom(let value): 52 | return value 53 | } 54 | } 55 | 56 | /// A `String` containing the username to be displayed. 57 | var displayUsername: String { 58 | return "@\(username)" 59 | } 60 | 61 | /// The `URL` to open the profile on the platform. 62 | var url: URL { 63 | switch platform { 64 | case .bluesky: 65 | return URL(string: "https://bsky.app/profile/\(username)")! 66 | case .facebook: 67 | return URL(string: "https://facebook.com/\(username)")! 68 | case .instagram: 69 | return URL(string: "https://instagram.com/\(username)")! 70 | case .linkedIn(let profileType): 71 | return URL(string: "https://linkedin.com/\(profileType.urlInfo)/\(username)")! 72 | case .mastodon(let instance): 73 | return URL(string: "https://\(instance)/@\(username)")! 74 | case .pinterest: 75 | return URL(string: "https://pinterest.co.uk/\(username)")! 76 | case .reddit: 77 | return URL(string: "https://reddit.com/user/\(username)")! 78 | case .snapchat: 79 | return URL(string: "https://snapchat.com/add/\(username)")! 80 | case .threads: 81 | return URL(string: "https://www.threads.net/@\(username)")! 82 | case .tikTok: 83 | return URL(string: "https://tiktok.com/@\(username)")! 84 | case .twitter: 85 | return URL(string: "https://twitter.com/\(username)")! 86 | case .x: 87 | return URL(string: "https://x.com/\(username)")! 88 | 89 | } 90 | } 91 | 92 | /// An example `AKProfile` to be used in SwiftUI previews. 93 | public static let example = AKProfile(username: "ExampleApp", platform: .reddit) 94 | } 95 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKProfileDisplayMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKProfileDisplayMode.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 26/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom enum that is used to determine how much data to show for a social platform. 11 | public enum AKProfileDisplayMode: Hashable, Sendable { 12 | case combined 13 | case platformOnly 14 | case usernameOnly 15 | case custom(_ value: String) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKProfilePlatform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKProfilePlatform.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 26/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom enum used for a social profile platform. 11 | public enum AKProfilePlatform: Hashable, Sendable { 12 | case bluesky 13 | case facebook 14 | case instagram 15 | case linkedIn(profileType: LinkedInProfileType) 16 | case mastodon(instance: String) 17 | case reddit 18 | case pinterest 19 | case snapchat 20 | case threads 21 | case tikTok 22 | case twitter 23 | case x 24 | 25 | /// A `String` containing the name of the platform. 26 | var name: String { 27 | switch self { 28 | case .bluesky: 29 | return "Bluesky" 30 | case .facebook: 31 | return "Facebook" 32 | case .instagram: 33 | return "Instagram" 34 | case .linkedIn(_): 35 | return "LinkedIn" 36 | case .mastodon(_): 37 | return "Mastodon" 38 | case .pinterest: 39 | return "Pinterest" 40 | case .reddit: 41 | return "Reddit" 42 | case .snapchat: 43 | return "Snapchat" 44 | case .threads: 45 | return "Threads" 46 | case .tikTok: 47 | return "TikTok" 48 | case .twitter: 49 | return "Twitter" 50 | case .x: 51 | return "X" 52 | } 53 | } 54 | 55 | /// A `String` containing the name of the image to use for the platform. 56 | var imageName: String { 57 | switch self { 58 | case .bluesky: 59 | return "bluesky" 60 | case .facebook: 61 | return "facebook" 62 | case .instagram: 63 | return "instagram" 64 | case .linkedIn(_): 65 | return "linkedin" 66 | case .mastodon(_): 67 | return "mastodon" 68 | case .pinterest: 69 | return "pinterest" 70 | case .reddit: 71 | return "reddit" 72 | case .snapchat: 73 | return "snapchat" 74 | case .threads: 75 | return "threads" 76 | case .tikTok: 77 | return "tiktok" 78 | case .twitter: 79 | return "twitter" 80 | case .x: 81 | return "x" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AKShowOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKShowOption.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 29/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom enum used to indicate whether a feature should be shown or hidden. 11 | public enum AKShowOption: Sendable { 12 | case always, testFlightOnly, appStoreOnly, never 13 | 14 | var isVisible: Bool { 15 | switch self { 16 | case .always: 17 | return true 18 | case .testFlightOnly: 19 | return Bundle.main.userType == .testFlight 20 | case .appStoreOnly: 21 | return Bundle.main.userType == .appStore 22 | case .never: 23 | return false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/AboutKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Ext.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 24/02/2021. 6 | // 7 | 8 | #if os(macOS) 9 | import AppKit 10 | import IOKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | @MainActor 16 | struct AboutKit { 17 | 18 | private init() {} 19 | 20 | #if os(macOS) || targetEnvironment(macCatalyst) 21 | 22 | /// Returns a `String` containing the identifier of the current device, e.g. MacBookPro13,1 23 | private static let deviceType: String = { 24 | let service = IOServiceGetMatchingService( 25 | kIOMainPortDefault, 26 | IOServiceMatching("IOPlatformExpertDevice") 27 | ) 28 | 29 | var modelIdentifier: String? 30 | 31 | if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data { 32 | modelIdentifier = String(data: modelData, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters) 33 | } 34 | 35 | IOObjectRelease(service) 36 | 37 | return modelIdentifier ?? "Unknown" 38 | }() 39 | 40 | /// A `String` containing debug details about the current app. 41 | static let debugDetails: String = { 42 | let versionNumber = Bundle.main.versionNumber 43 | let buildNumber = Bundle.main.buildNumber 44 | let versionDetails = "App Version: \(versionNumber) (\(buildNumber))" 45 | 46 | let osDetails = "OS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)" 47 | let deviceDetails = "Device: \(deviceType)" 48 | let environmentDetails = "Environment: \(Bundle.main.userType.title)" 49 | 50 | return "\n\n\nDEBUG DETAILS\n\n\(versionDetails)\n\(osDetails)\n\(deviceDetails)\n\(environmentDetails)" 51 | }() 52 | 53 | #elseif os(iOS) || os(visionOS) 54 | 55 | /// Returns a `String` containing the identifier of the current device, e.g. iPhone17,1 56 | private static let deviceType: String = { 57 | if ProcessInfo().isiOSAppOnMac { 58 | var size = 0 59 | let key = "hw.model" 60 | sysctlbyname(key, nil, &size, nil, 0) 61 | var value = [CChar](repeating: 0, count: size) 62 | sysctlbyname(key, &value, &size, nil, 0) 63 | return String(cString: value, encoding: .utf8) ?? "Mac" 64 | } else { 65 | return UIDevice.current.deviceType 66 | } 67 | }() 68 | 69 | /// A `String` containing debug details about the current app. 70 | static let debugDetails: String = { 71 | let versionNumber = Bundle.main.versionNumber 72 | let buildNumber = Bundle.main.buildNumber 73 | let versionDetails = "App Version: \(versionNumber) (\(buildNumber))" 74 | 75 | let osDetails = "OS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)" 76 | let deviceDetails = "Device: \(deviceType)" 77 | let environmentDetails = "Environment: \(Bundle.main.userType.title)" 78 | 79 | return "\n\n\nDEBUG DETAILS\n\n\(versionDetails)\n\(osDetails)\n\(deviceDetails)\n\(environmentDetails)" 80 | }() 81 | 82 | #endif 83 | } 84 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/LinkedInProfileType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkedInProfileType.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 26/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom enum related to the type of profile a LinkedIn account is. 11 | public enum LinkedInProfileType: Sendable { 12 | case company, user 13 | 14 | /// A `String` to be used in the LinkedIn profile URL. 15 | var urlInfo: String { 16 | switch self { 17 | case .company: 18 | return "company" 19 | case .user: 20 | return "in" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/PlatformImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlatformImage.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(macOS) 11 | extension NSImage: @retroactive @unchecked Sendable {} 12 | public typealias PlatformImage = NSImage 13 | #else 14 | public typealias PlatformImage = UIImage 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/RemoteImageLoadState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteImageLoadState.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 03/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RemoteImageLoadState { 11 | case loading 12 | case error 13 | case loaded(image: PlatformImage) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/AboutKit/Models/UserType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserType.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 12/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A custom enum containing the environment the current user is using. 11 | enum UserType { 12 | case debug, testFlight, appStore 13 | 14 | /// Returns a `String` matching the environment. 15 | var title: String { 16 | switch self { 17 | case .debug: 18 | return "Debug" 19 | case .testFlight: 20 | return "TestFlight" 21 | case .appStore: 22 | return "App Store" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/AboutKit/Networking/AppIconNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIconNetworkManager.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A singleton providing the ability to perform a network request which 11 | /// looks up an app based on its app ID to get the URL of its app icon. 12 | /// The app icon URL is then cached for quickly accessing with future 13 | /// requests to save making another network call. 14 | @MainActor 15 | final class AppIconNetworkManager { 16 | /// Creates the singleton object. 17 | static let shared = AppIconNetworkManager() 18 | 19 | /// Creates the cache object for storing the app icon URL. 20 | private let cache = NSCache() 21 | 22 | private init() {} 23 | 24 | /// Fetches the app icon URL string for a given app. 25 | /// - Parameter app: The app to retrieve the app icon for. 26 | /// - Returns: The string containing the URL of the app's icon. 27 | func fetchURL(for app: any AKApp) async -> String? { 28 | let urlString = "https://itunes.apple.com/lookup?id=\(app.id)" 29 | let cacheKey = NSString(string: urlString) 30 | 31 | guard let url = URL(string: urlString) else { 32 | return nil 33 | } 34 | 35 | if let cachedImageURL = cache.object(forKey: cacheKey) as? String { 36 | return cachedImageURL 37 | 38 | } else { 39 | do { 40 | let (data, _) = try await URLSession.shared.data(from: url) 41 | 42 | guard let decodedResponse = try? JSONDecoder().decode(AppResponse.self, from: data), 43 | let appIconURL = decodedResponse.results.first?.appIcon else { 44 | return nil 45 | } 46 | 47 | cache.setObject(NSString(string: appIconURL), forKey: cacheKey) 48 | return appIconURL 49 | 50 | } catch { 51 | return nil 52 | } 53 | } 54 | } 55 | 56 | /// A custom struct containing the results from the request. 57 | struct AppResponse: Codable { 58 | let results: [AppResult] 59 | } 60 | 61 | /// A custom struct containing the appIcon URL string in the result. 62 | struct AppResult: Codable { 63 | let appIcon: String 64 | 65 | enum CodingKeys: String, CodingKey { 66 | case appIcon = "artworkUrl512" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/AboutKit/Protocols/AKApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKApp.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 19/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public protocol AKApp: Identifiable, Sendable { 11 | var id: String { get } 12 | var name: String { get } 13 | var appIcon: PlatformImage? { get } 14 | } 15 | 16 | public extension AKApp { 17 | /// The App Store URL of the app based on its ID. 18 | var appStoreURL: URL { 19 | #if os(macOS) 20 | URL(string: "macappstore://apps.apple.com/app/id\(id)")! 21 | #else 22 | URL(string: "https://apps.apple.com/app/id\(id)")! 23 | #endif 24 | } 25 | 26 | /// The App Store URL of the app based on its ID for sharing. 27 | var appStoreShareURL: URL { 28 | URL(string: "https://apps.apple.com/app/id\(id)")! 29 | } 30 | 31 | /// The App Store URL to review the app based on its ID. 32 | var appStoreReviewURL: URL { 33 | #if os(macOS) 34 | URL(string: "macappstore://apps.apple.com/app/id\(id)?action=write-review")! 35 | #else 36 | URL(string: "https://apps.apple.com/app/id\(id)?action=write-review")! 37 | #endif 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/bluesky.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "bluesky.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/bluesky.symbolset/bluesky.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | Weight/Scale Variations 20 | Ultralight 21 | Thin 22 | Light 23 | Regular 24 | Medium 25 | Semibold 26 | Bold 27 | Heavy 28 | Black 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Design Variations 40 | Symbols are supported in up to nine weights and three scales. 41 | For optimal layout with text and other symbols, vertically align 42 | symbols with the adjacent text. 43 | 44 | 45 | 46 | 47 | 48 | Margins 49 | Leading and trailing margins on the left and right side of each symbol 50 | can be adjusted by modifying the x-location of the margin guidelines. 51 | Modifications are automatically applied proportionally to all 52 | scales and weights. 53 | 54 | 55 | 56 | Exporting 57 | Symbols should be outlined when exporting to ensure the 58 | design is preserved when submitting to Xcode. 59 | Template v.6.0 60 | Requires Xcode 16 or greater 61 | Generated from bluesky 62 | Typeset at 100.0 points 63 | Small 64 | Medium 65 | Large 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/facebook.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "facebook.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/facebook.symbolset/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | Weight/Scale Variations 16 | Ultralight 17 | Thin 18 | Light 19 | Regular 20 | Medium 21 | Semibold 22 | Bold 23 | Heavy 24 | Black 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Design Variations 36 | Symbols are supported in up to nine weights and three scales. 37 | For optimal layout with text and other symbols, vertically align 38 | symbols with the adjacent text. 39 | 40 | 41 | 42 | 43 | 44 | Margins 45 | Leading and trailing margins on the left and right side of each symbol 46 | can be adjusted by modifying the x-location of the margin guidelines. 47 | Modifications are automatically applied proportionally to all 48 | scales and weights. 49 | 50 | 51 | 52 | Exporting 53 | Symbols should be outlined when exporting to ensure the 54 | design is preserved when submitting to Xcode. 55 | Template v.4.0 56 | Requires Xcode 14 or greater 57 | Generated from facebook 58 | Typeset at 100 points 59 | Small 60 | Medium 61 | Large 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/instagram.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "instagram.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/instagram.symbolset/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | Weight/Scale Variations 16 | Ultralight 17 | Thin 18 | Light 19 | Regular 20 | Medium 21 | Semibold 22 | Bold 23 | Heavy 24 | Black 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Design Variations 36 | Symbols are supported in up to nine weights and three scales. 37 | For optimal layout with text and other symbols, vertically align 38 | symbols with the adjacent text. 39 | 40 | 41 | 42 | 43 | 44 | Margins 45 | Leading and trailing margins on the left and right side of each symbol 46 | can be adjusted by modifying the x-location of the margin guidelines. 47 | Modifications are automatically applied proportionally to all 48 | scales and weights. 49 | 50 | 51 | 52 | Exporting 53 | Symbols should be outlined when exporting to ensure the 54 | design is preserved when submitting to Xcode. 55 | Template v.4.0 56 | Requires Xcode 14 or greater 57 | Generated from instagram 58 | Typeset at 100 points 59 | Small 60 | Medium 61 | Large 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/linkedin.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "linkedin.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/mastodon.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "mastodon.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/pinterest.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "pinterest.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/pinterest.symbolset/pinterest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | Weight/Scale Variations 16 | Ultralight 17 | Thin 18 | Light 19 | Regular 20 | Medium 21 | Semibold 22 | Bold 23 | Heavy 24 | Black 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Design Variations 36 | Symbols are supported in up to nine weights and three scales. 37 | For optimal layout with text and other symbols, vertically align 38 | symbols with the adjacent text. 39 | 40 | 41 | 42 | 43 | 44 | Margins 45 | Leading and trailing margins on the left and right side of each symbol 46 | can be adjusted by modifying the x-location of the margin guidelines. 47 | Modifications are automatically applied proportionally to all 48 | scales and weights. 49 | 50 | 51 | 52 | Exporting 53 | Symbols should be outlined when exporting to ensure the 54 | design is preserved when submitting to Xcode. 55 | Template v.4.0 56 | Requires Xcode 14 or greater 57 | Generated from pinterest 58 | Typeset at 100 points 59 | Small 60 | Medium 61 | Large 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/reddit.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "reddit.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/snapchat.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "snapchat.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/threads.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "threads.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/tiktok.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "tiktok.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/tiktok.symbolset/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | Weight/Scale Variations 16 | Ultralight 17 | Thin 18 | Light 19 | Regular 20 | Medium 21 | Semibold 22 | Bold 23 | Heavy 24 | Black 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Design Variations 36 | Symbols are supported in up to nine weights and three scales. 37 | For optimal layout with text and other symbols, vertically align 38 | symbols with the adjacent text. 39 | 40 | 41 | 42 | 43 | 44 | Margins 45 | Leading and trailing margins on the left and right side of each symbol 46 | can be adjusted by modifying the x-location of the margin guidelines. 47 | Modifications are automatically applied proportionally to all 48 | scales and weights. 49 | 50 | 51 | 52 | Exporting 53 | Symbols should be outlined when exporting to ensure the 54 | design is preserved when submitting to Xcode. 55 | Template v.4.0 56 | Requires Xcode 14 or greater 57 | Generated from tiktok 58 | Typeset at 100 points 59 | Small 60 | Medium 61 | Large 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/twitter.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "twitter.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/x.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "x.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/Assets.xcassets/x.symbolset/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | Weight/Scale Variations 20 | Ultralight 21 | Thin 22 | Light 23 | Regular 24 | Medium 25 | Semibold 26 | Bold 27 | Heavy 28 | Black 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Design Variations 40 | Symbols are supported in up to nine weights and three scales. 41 | For optimal layout with text and other symbols, vertically align 42 | symbols with the adjacent text. 43 | 44 | 45 | 46 | 47 | 48 | Margins 49 | Leading and trailing margins on the left and right side of each symbol 50 | can be adjusted by modifying the x-location of the margin guidelines. 51 | Modifications are automatically applied proportionally to all 52 | scales and weights. 53 | 54 | 55 | 56 | Exporting 57 | Symbols should be outlined when exporting to ensure the 58 | design is preserved when submitting to Xcode. 59 | Template v.4.0 60 | Requires Xcode 14 or greater 61 | Generated from x 62 | Typeset at 100.0 points 63 | Small 64 | Medium 65 | Large 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Sources/AboutKit/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/AboutKit/UIKit Views/MailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MailView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | #if os(iOS) || os(visionOS) 9 | import SwiftUI 10 | import MessageUI 11 | 12 | /// A `UIViewControllerRepresentable` that shows the default iOS mail sheet 13 | /// with some pre-configured fields based on the current app. 14 | struct MailView: UIViewControllerRepresentable { 15 | @Environment(\.dismiss) private var dismiss 16 | 17 | /// The `AKMyApp` to use for providing data to show in the Mail sheet. 18 | private let app: AKMyApp 19 | 20 | /// A `String` containing some debug information that will be sent to the developer. 21 | private let debugDetails: String 22 | 23 | /// Initializes a `UIViewControllerRepresentable` that shows the default iOS mail sheet 24 | /// with some pre-configured fields based on the current app. 25 | /// - Parameters: 26 | /// - app: The `AKMyApp` to use for providing data to show in the Mail sheet. 27 | /// - debugDetails: A `String` containing some debug information that will be sent to the developer. 28 | init(app: AKMyApp, debugDetails: String) { 29 | self.app = app 30 | self.debugDetails = debugDetails 31 | } 32 | 33 | class Coordinator: NSObject, @preconcurrency MFMailComposeViewControllerDelegate { 34 | private var dismiss: DismissAction 35 | 36 | init(dismiss: DismissAction) { 37 | self.dismiss = dismiss 38 | } 39 | 40 | @MainActor 41 | func mailComposeController( 42 | _ controller: MFMailComposeViewController, 43 | didFinishWith result: MFMailComposeResult, 44 | error: Error? 45 | ) { 46 | dismiss() 47 | } 48 | } 49 | 50 | func makeCoordinator() -> Coordinator { 51 | return Coordinator(dismiss: dismiss) 52 | } 53 | 54 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> MFMailComposeViewController { 55 | let mailComposerViewController = MFMailComposeViewController() 56 | mailComposerViewController.mailComposeDelegate = context.coordinator 57 | 58 | if let email = app.email { 59 | mailComposerViewController.setToRecipients([email]) 60 | } 61 | 62 | mailComposerViewController.setSubject("\(app.name) - Support") 63 | mailComposerViewController.setMessageBody(debugDetails, isHTML: false) 64 | 65 | return mailComposerViewController 66 | } 67 | 68 | func updateUIViewController( 69 | _ uiViewController: MFMailComposeViewController, 70 | context: UIViewControllerRepresentableContext 71 | ) {} 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /Sources/AboutKit/UIKit Views/ShareSheetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareSheetView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | #if os(iOS) || os(visionOS) 9 | import SwiftUI 10 | 11 | /// A `UIViewControllerRepresentable` that shows the default iOS share sheet 12 | /// with some details on how to install the current app. 13 | struct ShareSheetView: UIViewControllerRepresentable { 14 | 15 | /// The `AKMyApp` to use for showing in the download message. 16 | private let app: AKMyApp 17 | 18 | /// Initializes a `UIViewControllerRepresentable` that shows the default iOS share sheet 19 | /// with some details on how to install the current app. 20 | /// - Parameter app: The `AKMyApp` to use for showing in the download message. 21 | init(app: AKMyApp) { 22 | self.app = app 23 | } 24 | 25 | func makeUIViewController( 26 | context: UIViewControllerRepresentableContext 27 | ) -> UIActivityViewController { 28 | let message = String( 29 | localized: "Check out \(app.name) on the App Store!", 30 | bundle: .module 31 | ) 32 | 33 | return UIActivityViewController( 34 | activityItems: [app.appStoreShareURL, message], 35 | applicationActivities: nil 36 | ) 37 | } 38 | 39 | func updateUIViewController( 40 | _ uiViewController: UIActivityViewController, 41 | context: UIViewControllerRepresentableContext 42 | ) {} 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/AboutKit/Utilities/LocalizedStrings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedStrings.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 25/02/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Provides a quick way to access localized strings stored in the package bundle. 11 | enum LocalizedStrings { 12 | static let email = String(localized: "Email", bundle: .module) 13 | static let website = String(localized: "Website", bundle: .module) 14 | static let shareApp = String(localized: "Share App", bundle: .module) 15 | static let writeReview = String(localized: "Write Review", bundle: .module) 16 | static let privacyPolicy = String(localized: "Privacy Policy", bundle: .module) 17 | static let termsOfUse = String(localized: "Terms Of Use", bundle: .module) 18 | static let testFlight = String(localized: "TestFlight", bundle: .module) 19 | static let acknowledgements = String(localized: "Acknowledgements", bundle: .module) 20 | 21 | static let contactDeveloper = String(localized: "Contact Developer…", bundle: .module) 22 | static let openWebsite = String(localized: "Open Website…", bundle: .module) 23 | static let viewProfile = String(localized: "View Profile…", bundle: .module) 24 | static let share = String(localized: "Share…", bundle: .module) 25 | static let review = String(localized: "Review…", bundle: .module) 26 | static let viewPrivacyPolicy = String(localized: "View Privacy Policy…", bundle: .module) 27 | static let viewTermsOfUse = String(localized: "View Terms of Use…", bundle: .module) 28 | static let openTestFlight = String(localized: "Open TestFlight…", bundle: .module) 29 | static let viewAcknowledgements = String(localized: "View Acknowledgements…", bundle: .module) 30 | 31 | static let otherApps = String(localized: "Other Apps", bundle: .module) 32 | static let view = String(localized: "View", bundle: .module) 33 | static let viewAllApps = String(localized: "View all Apps", bundle: .module) 34 | static let viewMac = String(localized: "View…", bundle: .module) 35 | 36 | static let aboutApp = String(localized: "About App", bundle: .module) 37 | static let done = String(localized: "Done", bundle: .module) 38 | 39 | static let people = String(localized: "People", bundle: .module) 40 | static let frameworks = String(localized: "Frameworks", bundle: .module) 41 | static let links = String(localized: "Links", bundle: .module) 42 | static let profiles = String(localized: "Profiles", bundle: .module) 43 | static let details = String(localized: "Details", bundle: .module) 44 | static let productPage = String(localized: "Product Page", bundle: .module) 45 | static let repository = String(localized: "Repository", bundle: .module) 46 | static let compliance = String(localized: "Compliance", bundle: .module) 47 | } 48 | -------------------------------------------------------------------------------- /Sources/AboutKit/Utilities/RemoteImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteImageLoader.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 03/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor 11 | final class RemoteImageLoader: ObservableObject { 12 | @Published var loadState = RemoteImageLoadState.loading 13 | 14 | private let cache = NSCache() 15 | 16 | init(url: String) { 17 | Task { 18 | await loadAppIcon(for: url) 19 | } 20 | } 21 | 22 | private func loadAppIcon( 23 | for urlString: String 24 | ) async { 25 | loadState = .loading 26 | 27 | let cacheKey = NSString(string: urlString) 28 | 29 | guard let url = URL(string: urlString) else { 30 | loadState = .error 31 | return 32 | } 33 | 34 | if let cachedImage = cache.object(forKey: cacheKey) { 35 | loadState = .loaded(image: cachedImage) 36 | } else { 37 | do { 38 | let (data, _) = try await URLSession.shared.data(from: url) 39 | 40 | guard let image = PlatformImage(data: data) else { 41 | loadState = .error 42 | return 43 | } 44 | 45 | cache.setObject(image, forKey: cacheKey) 46 | loadState = .loaded(image: image) 47 | 48 | } catch { 49 | loadState = .error 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/AcknowledgementsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AcknowledgementsView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AcknowledgementsView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | 13 | private let acknowledgments: AKAcknowledgements 14 | 15 | init(_ acknowledgments: AKAcknowledgements) { 16 | self.acknowledgments = acknowledgments 17 | } 18 | 19 | var body: some View { 20 | #if os(macOS) 21 | NavigationStack { 22 | acknowledgementsForm 23 | .toolbar { 24 | doneToolbarItem 25 | } 26 | } 27 | .frame(minWidth: 400, minHeight: 300) 28 | 29 | #else 30 | acknowledgementsForm 31 | #endif 32 | } 33 | 34 | private var acknowledgementsForm: some View { 35 | Form { 36 | if let people = acknowledgments.people { 37 | Section { 38 | ForEach(Array(people.enumerated()), id: \.0) { index, person in 39 | NavigationLink { 40 | PersonAcknowledgementView(person) 41 | } label: { 42 | #if os(iOS) || os(visionOS) 43 | Label(person.name, systemImage: "person") 44 | #else 45 | Text(person.name) 46 | #endif 47 | } 48 | .id("person-\(index)") 49 | } 50 | 51 | } header: { 52 | Text(LocalizedStrings.people) 53 | } 54 | } 55 | 56 | if let frameworks = acknowledgments.frameworks { 57 | Section { 58 | ForEach(Array(frameworks.enumerated()), id: \.0) { index, acknowledgment in 59 | NavigationLink { 60 | FrameworkAcknowledgementView(acknowledgment) 61 | } label: { 62 | #if os(iOS) || os(visionOS) 63 | Label(acknowledgment.name, systemImage: "square.stack.3d.up") 64 | #else 65 | Text(acknowledgment.name) 66 | #endif 67 | } 68 | .id("framework-\(index)") 69 | } 70 | 71 | } header: { 72 | Text(LocalizedStrings.frameworks) 73 | } 74 | } 75 | } 76 | #if os(macOS) 77 | .formStyle(.grouped) 78 | #endif 79 | .navigationTitle(LocalizedStrings.acknowledgements) 80 | #if os(iOS) || os(visionOS) || os(watchOS) 81 | .navigationBarTitleDisplayMode(.inline) 82 | #endif 83 | } 84 | 85 | private var doneToolbarItem: some ToolbarContent { 86 | ToolbarItem(placement: .cancellationAction) { 87 | Button(LocalizedStrings.done) { 88 | dismiss() 89 | } 90 | } 91 | } 92 | } 93 | 94 | struct AcknowledgementsView_Previews: PreviewProvider { 95 | static var previews: some View { 96 | AcknowledgementsView(.example) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/FrameworkAcknowledgementView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameworkAcknowledgementView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FrameworkAcknowledgementView: View { 11 | @Environment(\.openURL) private var openURL 12 | 13 | private let acknowledgment: AKFrameworkAcknowledgement 14 | 15 | init(_ acknowledgment: AKFrameworkAcknowledgement) { 16 | self.acknowledgment = acknowledgment 17 | } 18 | 19 | var body: some View { 20 | Form { 21 | if let links = acknowledgment.links, 22 | links.isEmpty == false { 23 | Section { 24 | ForEach(Array(links.enumerated()), id: \.0) { _, link in 25 | #if os(iOS) || os(visionOS) 26 | Button { 27 | openURL(link.url) 28 | } label: { 29 | ItemLabel( 30 | link.title, 31 | systemImage: link.systemImage 32 | ) 33 | } 34 | 35 | #elseif os(macOS) 36 | ItemLabel(link.title, actionTitle: LocalizedStrings.viewMac) { 37 | openURL(link.url) 38 | } 39 | 40 | #elseif os(tvOS) || os(watchOS) 41 | ItemLabel( 42 | link.title, 43 | details: link.url.absoluteString 44 | ) 45 | #endif 46 | } 47 | } header: { 48 | Text(LocalizedStrings.links) 49 | } 50 | } 51 | 52 | if let details = acknowledgment.details { 53 | Section { 54 | Text(details) 55 | .lineLimit(nil) 56 | #if os(iOS) || os(visionOS) 57 | .font(.system(.subheadline, design: .monospaced)) 58 | .frame(maxWidth: .infinity, alignment: .leading) 59 | .listRowInsets(.init(top: 12, leading: 12, bottom: 12, trailing: 12)) 60 | #elseif os(macOS) 61 | .font(.system(.subheadline, design: .monospaced)) 62 | #elseif os(tvOS) || os(watchOS) 63 | .font(.system(.caption2, design: .monospaced)) 64 | .frame(maxWidth: .infinity, alignment: .leading) 65 | .listRowBackground(Color.clear) 66 | #endif 67 | } header: { 68 | Text(LocalizedStrings.details) 69 | } 70 | } 71 | } 72 | #if os(macOS) 73 | .formStyle(.grouped) 74 | #endif 75 | .navigationTitle(acknowledgment.name) 76 | #if os(iOS) || os(watchOS) 77 | .navigationBarTitleDisplayMode(.inline) 78 | #endif 79 | } 80 | } 81 | 82 | struct FrameworkAcknowledgementView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | FrameworkAcknowledgementView(.example) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HeaderView: View { 11 | private let app: AKMyApp 12 | 13 | @State private var appIconURL: String? 14 | 15 | init(app: AKMyApp) { 16 | self.app = app 17 | } 18 | 19 | var body: some View { 20 | VStack(spacing: 8) { 21 | appIcon 22 | 23 | Text("\(app.name) \(Bundle.main.versionNumber) (\(Bundle.main.buildNumber))") 24 | .font(.headline) 25 | .padding(.top) 26 | 27 | Text(app.developer.name) 28 | .foregroundStyle(.secondary) 29 | } 30 | .multilineTextAlignment(.center) 31 | .frame(maxWidth: .infinity) 32 | .listRowBackground(Color.clear) 33 | .task { 34 | await loadAppIcon() 35 | } 36 | } 37 | 38 | private var appIcon: some View { 39 | ZStack { 40 | #if os(iOS) 41 | Color(.secondarySystemGroupedBackground) 42 | #elseif os(watchOS) || os(tvOS) || os(visionOS) 43 | Color.white.opacity(0.2) 44 | #endif 45 | 46 | if let appIcon = app.appIcon { 47 | #if os(macOS) 48 | Image(nsImage: appIcon) 49 | .resizable() 50 | .scaledToFit() 51 | #else 52 | Image(uiImage: appIcon) 53 | .resizable() 54 | .scaledToFit() 55 | #endif 56 | 57 | } else if let appIconURL = appIconURL { 58 | RemoteImageView(url: appIconURL) 59 | .scaledToFit() 60 | } 61 | } 62 | .frame(width: appIconWidth, height: appIconHeight) 63 | #if os(iOS) || os(tvOS) 64 | .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 65 | #elseif os(visionOS) || os(watchOS) 66 | .clipShape(Circle()) 67 | #endif 68 | .accessibilityHidden(true) 69 | } 70 | 71 | private var appIconWidth: CGFloat { 72 | #if os(macOS) || os(watchOS) 73 | return 64 74 | #elseif os(tvOS) 75 | return 300 76 | #else 77 | return 100 78 | #endif 79 | } 80 | 81 | private var appIconHeight: CGFloat { 82 | #if os(macOS) || os(watchOS) 83 | return 64 84 | #elseif os(tvOS) 85 | return 180 86 | #else 87 | return 100 88 | #endif 89 | } 90 | 91 | private func loadAppIcon() async { 92 | if app.appIcon == nil { 93 | appIconURL = await AppIconNetworkManager.shared.fetchURL(for: app) 94 | } 95 | } 96 | } 97 | 98 | struct HeaderView_Previews: PreviewProvider { 99 | static var previews: some View { 100 | List { 101 | HeaderView(app: AKMyApp.example) 102 | .listRowBackground(Color.clear) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/ItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemLabel.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 08/09/2021. 6 | // 7 | 8 | #if os(iOS) || os(visionOS) 9 | import SwiftUI 10 | 11 | struct ItemLabel: View { 12 | private let title: String 13 | private let systemImageName: String? 14 | private let imageName: String? 15 | 16 | init(_ title: String, systemImage name: String) { 17 | self.title = title 18 | self.systemImageName = name 19 | self.imageName = nil 20 | } 21 | 22 | init(_ title: String, image name: String) { 23 | self.title = title 24 | self.systemImageName = nil 25 | self.imageName = name 26 | } 27 | 28 | var body: some View { 29 | Label { 30 | Text(title) 31 | .foregroundStyle(Color.primary) 32 | 33 | } icon: { 34 | Group { 35 | if let systemImageName { 36 | Image(systemName: systemImageName) 37 | } else if let imageName { 38 | Image(imageName, bundle: .module) 39 | } else { 40 | Image(systemName: "circle") 41 | } 42 | } 43 | .foregroundStyle(Color.accentColor) 44 | } 45 | } 46 | } 47 | 48 | struct ListButtonLabel_Previews: PreviewProvider { 49 | static var previews: some View { 50 | VStack { 51 | ItemLabel("Email", systemImage: "envelope") 52 | ItemLabel("Twitter", image: "twitter") 53 | } 54 | } 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/MacItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacItemLabel.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/03/2023. 6 | // 7 | 8 | #if os(macOS) 9 | import SwiftUI 10 | 11 | struct ItemLabel: View { 12 | private let title: String 13 | private let actionTitle: String 14 | private let action: () -> Void 15 | 16 | init( 17 | _ title: String, 18 | actionTitle: String, 19 | action: @escaping () -> Void 20 | ) { 21 | self.title = title 22 | self.actionTitle = actionTitle 23 | self.action = action 24 | } 25 | 26 | var body: some View { 27 | HStack { 28 | Text(title) 29 | Spacer() 30 | Button(actionTitle, action: action) 31 | } 32 | } 33 | } 34 | 35 | struct ItemLabel_Previews: PreviewProvider { 36 | static var previews: some View { 37 | Form { 38 | ItemLabel("Title", actionTitle: "Action") {} 39 | } 40 | .formStyle(.grouped) 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/MacOtherAppRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacOtherAppRow.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/03/2023. 6 | // 7 | 8 | #if os(macOS) 9 | import SwiftUI 10 | 11 | struct OtherAppRowView: View { 12 | @Environment(\.openURL) private var openURL 13 | 14 | private let otherApp: AKOtherApp 15 | 16 | @State private var appIconURL: String? 17 | 18 | init(_ otherApp: AKOtherApp) { 19 | self.otherApp = otherApp 20 | } 21 | 22 | var body: some View { 23 | HStack(spacing: 12) { 24 | appIcon 25 | 26 | Text(otherApp.name) 27 | .font(.headline) 28 | .lineLimit(2) 29 | 30 | Spacer(minLength: 1) 31 | 32 | viewOnAppStoreButton 33 | } 34 | .task { 35 | await loadAppIcon() 36 | } 37 | } 38 | 39 | private var appIcon: some View { 40 | ZStack { 41 | Color(.controlBackgroundColor) 42 | 43 | if let appIcon = otherApp.appIcon { 44 | Image(nsImage: appIcon) 45 | .resizable() 46 | .scaledToFit() 47 | 48 | } else if let appIconURL = appIconURL { 49 | RemoteImageView(url: appIconURL) 50 | .scaledToFit() 51 | } 52 | } 53 | .frame(width: 32, height: 32) 54 | .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) 55 | .accessibilityHidden(true) 56 | } 57 | 58 | private var viewOnAppStoreButton: some View { 59 | Button(LocalizedStrings.viewMac) { 60 | openURL(otherApp.appStoreURL) 61 | } 62 | .accessibilityLabel(String(localized: "View \(otherApp.name) in the App Store", bundle: .module)) 63 | } 64 | 65 | private func loadAppIcon() async { 66 | if otherApp.appIcon == nil { 67 | appIconURL = await AppIconNetworkManager.shared.fetchURL(for: otherApp) 68 | } 69 | } 70 | } 71 | 72 | struct OtherAppRowView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | Form { 75 | OtherAppRowView(AKOtherApp.example) 76 | } 77 | .formStyle(.grouped) 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/OtherAppRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherAppRowView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | #if os(iOS) || os(visionOS) 9 | import SwiftUI 10 | 11 | struct OtherAppRowView: View { 12 | private let otherApp: AKOtherApp 13 | 14 | @State private var appIconURL: String? 15 | 16 | init(_ otherApp: AKOtherApp) { 17 | self.otherApp = otherApp 18 | } 19 | 20 | var body: some View { 21 | HStack(spacing: 16) { 22 | appIcon 23 | 24 | Text(otherApp.name) 25 | .font(.headline) 26 | .lineLimit(2) 27 | 28 | Spacer(minLength: 1) 29 | 30 | viewOnAppStoreButton 31 | } 32 | .padding(.vertical, 8) 33 | .buttonStyle(.plain) 34 | .task { 35 | await loadAppIcon() 36 | } 37 | } 38 | 39 | private var appIcon: some View { 40 | ZStack { 41 | Color(.systemGroupedBackground) 42 | 43 | if let appIcon = otherApp.appIcon { 44 | Image(uiImage: appIcon) 45 | .resizable() 46 | .scaledToFit() 47 | 48 | } else if let appIconURL = appIconURL { 49 | RemoteImageView(url: appIconURL) 50 | .scaledToFit() 51 | } 52 | } 53 | .frame(width: 60, height: 60) 54 | #if os(visionOS) 55 | .clipShape(Circle()) 56 | #else 57 | .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) 58 | #endif 59 | .accessibilityHidden(true) 60 | } 61 | 62 | private var viewOnAppStoreButton: some View { 63 | Link(destination: otherApp.appStoreURL) { 64 | Text(LocalizedStrings.view) 65 | .font(.headline) 66 | .lineLimit(1) 67 | .padding(.horizontal, 8) 68 | #if os(iOS) && !targetEnvironment(macCatalyst) 69 | .hoverEffect(.lift) 70 | #endif 71 | .accessibilityLabel(String(localized: "View \(otherApp.name) in the App Store", bundle: .module)) 72 | } 73 | .buttonStyle(.bordered) 74 | .buttonBorderShape(.capsule) 75 | .controlSize(.small) 76 | } 77 | 78 | private func loadAppIcon() async { 79 | if otherApp.appIcon == nil { 80 | appIconURL = await AppIconNetworkManager.shared.fetchURL(for: otherApp) 81 | } 82 | } 83 | } 84 | 85 | struct OtherAppRowView_Previews: PreviewProvider { 86 | static var previews: some View { 87 | OtherAppRowView(AKOtherApp.example) 88 | } 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/PersonAcknowledgmentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersonAcknowledgementView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PersonAcknowledgementView: View { 11 | @Environment(\.openURL) private var openURL 12 | 13 | private let acknowledgment: AKPersonAcknowledgement 14 | 15 | init(_ acknowledgment: AKPersonAcknowledgement) { 16 | self.acknowledgment = acknowledgment 17 | } 18 | 19 | var body: some View { 20 | Form { 21 | if let profiles = acknowledgment.profiles, 22 | profiles.isEmpty == false { 23 | Section { 24 | ForEach(Array(profiles.enumerated()), id: \.0) { _, profile in 25 | #if os(iOS) || os(visionOS) 26 | Button { 27 | openURL(profile.url) 28 | } label: { 29 | ItemLabel( 30 | profile.title, 31 | image: profile.platform.imageName 32 | ) 33 | } 34 | 35 | #elseif os(macOS) 36 | ItemLabel(profile.title, actionTitle: LocalizedStrings.viewProfile) { 37 | openURL(profile.url) 38 | } 39 | 40 | #elseif os(tvOS) || os(watchOS) 41 | ItemLabel( 42 | profile.platform.name, 43 | details: profile.displayUsername 44 | ) 45 | #endif 46 | } 47 | } header: { 48 | Text(LocalizedStrings.profiles) 49 | } 50 | } 51 | 52 | if let details = acknowledgment.details { 53 | Section { 54 | Text(details) 55 | .lineLimit(nil) 56 | #if os(iOS) || os(visionOS) 57 | .font(.subheadline) 58 | .frame(maxWidth: .infinity, alignment: .leading) 59 | .listRowInsets(.init(top: 12, leading: 12, bottom: 12, trailing: 12)) 60 | #elseif os(macOS) 61 | .font(.subheadline) 62 | #elseif os(tvOS) || os(watchOS) 63 | .font(.caption2) 64 | .frame(maxWidth: .infinity, alignment: .leading) 65 | .listRowBackground(Color.clear) 66 | #endif 67 | } header: { 68 | Text(LocalizedStrings.details) 69 | } 70 | } 71 | } 72 | #if os(macOS) 73 | .formStyle(.grouped) 74 | #endif 75 | .navigationTitle(acknowledgment.name) 76 | #if os(iOS) || os(visionOS) || os(watchOS) 77 | .navigationBarTitleDisplayMode(.inline) 78 | #endif 79 | } 80 | } 81 | 82 | struct PersonAcknowledgementView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | NavigationView { 85 | PersonAcknowledgementView(.example) 86 | } 87 | #if os(iOS) 88 | .navigationViewStyle(.stack) 89 | #endif 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/RemoteImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteImageView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RemoteImageView: View { 11 | @StateObject private var imageLoader: RemoteImageLoader 12 | 13 | init(url: String) { 14 | _imageLoader = StateObject( 15 | wrappedValue: RemoteImageLoader(url: url) 16 | ) 17 | } 18 | 19 | var body: some View { 20 | downloadedImage.resizable() 21 | } 22 | 23 | private var downloadedImage: Image { 24 | switch imageLoader.loadState { 25 | case .loading, .error: 26 | #if os(macOS) 27 | return Image(nsImage: NSImage()) 28 | #else 29 | return Image(uiImage: UIImage()) 30 | #endif 31 | case .loaded(let image): 32 | #if os(macOS) 33 | return Image(nsImage: image) 34 | #else 35 | return Image(uiImage: image) 36 | #endif 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/SectionHeaderLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeaderLabel.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 19/10/2023. 6 | // 7 | 8 | #if os(tvOS) 9 | import SwiftUI 10 | 11 | struct SectionHeaderLabel: View { 12 | private let title: String 13 | 14 | init(_ title: String) { 15 | self.title = title 16 | } 17 | 18 | var body: some View { 19 | Text(title) 20 | .textCase(nil) 21 | .font(.body.bold()) 22 | .foregroundStyle(.secondary) 23 | .padding(.top, 20) 24 | .padding(.bottom, 4) 25 | } 26 | } 27 | 28 | struct SectionHeaderLabel_Previews: PreviewProvider { 29 | static var previews: some View { 30 | Form { 31 | Section { 32 | Text("Content") 33 | } header: { 34 | SectionHeaderLabel("Title") 35 | } 36 | } 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/TVItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVItemLabel.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 13/05/2022. 6 | // 7 | 8 | #if os(tvOS) 9 | import SwiftUI 10 | 11 | struct ItemLabel: View { 12 | private let title: String 13 | private let details: String 14 | 15 | init(_ title: String, details: String) { 16 | self.title = title 17 | self.details = details 18 | } 19 | 20 | var body: some View { 21 | // Adding an empty button here so that the row can be 22 | // highlighted in a SwiftUI form. 23 | Button(action: {}) { 24 | HStack { 25 | Text(title) 26 | Spacer() 27 | 28 | Text(details) 29 | .foregroundStyle(.secondary) 30 | } 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | } 33 | } 34 | } 35 | 36 | struct ItemLabel_Previews: PreviewProvider { 37 | static var previews: some View { 38 | Form { 39 | ItemLabel("Title", details: "Details") 40 | } 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/TVOtherAppRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVOtherAppRowView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 19/07/2022. 6 | // 7 | 8 | #if os(tvOS) 9 | import SwiftUI 10 | 11 | struct OtherAppRowView: View { 12 | private let otherApp: AKOtherApp 13 | 14 | @State private var appIconURL: String? 15 | 16 | init(_ otherApp: AKOtherApp) { 17 | self.otherApp = otherApp 18 | } 19 | 20 | var body: some View { 21 | Link(destination: otherApp.appStoreURL) { 22 | HStack(spacing: 28) { 23 | appIcon 24 | 25 | ItemLabel( 26 | otherApp.name, 27 | details: LocalizedStrings.view 28 | ) 29 | } 30 | .padding(.vertical, 8) 31 | 32 | } 33 | .buttonStyle(.plain) 34 | .task { 35 | await loadAppIcon() 36 | } 37 | } 38 | 39 | private var appIcon: some View { 40 | ZStack { 41 | Color.secondary.opacity(0.2) 42 | 43 | if let appIcon = otherApp.appIcon { 44 | Image(uiImage: appIcon) 45 | .resizable() 46 | .scaledToFit() 47 | 48 | } else if let appIconURL = appIconURL { 49 | RemoteImageView(url: appIconURL) 50 | .scaledToFit() 51 | } 52 | } 53 | .frame(width: 150, height: 90) 54 | .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) 55 | .accessibilityHidden(true) 56 | } 57 | 58 | private func loadAppIcon() async { 59 | if otherApp.appIcon == nil { 60 | appIconURL = await AppIconNetworkManager.shared.fetchURL(for: otherApp) 61 | } 62 | } 63 | } 64 | 65 | struct OtherAppRowView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | Form { 68 | OtherAppRowView(.example) 69 | } 70 | } 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/WatchItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WatchItemLabel.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/03/2022. 6 | // 7 | 8 | #if os(watchOS) 9 | import SwiftUI 10 | 11 | struct ItemLabel: View { 12 | private let title: String 13 | private let details: String 14 | 15 | init(_ title: String, details: String) { 16 | self.title = title 17 | self.details = details 18 | } 19 | 20 | var body: some View { 21 | VStack(alignment: .leading, spacing: 4) { 22 | Text(title) 23 | 24 | Text(details) 25 | .font(.footnote) 26 | .foregroundStyle(.secondary) 27 | } 28 | .frame(maxWidth: .infinity, alignment: .leading) 29 | .padding(.vertical, 8) 30 | } 31 | } 32 | 33 | struct ItemLabel_Previews: PreviewProvider { 34 | static var previews: some View { 35 | ItemLabel("Title", details: "Details") 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/AboutKit/Views/WatchOtherAppRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WatchOtherAppRowView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 19/07/2022. 6 | // 7 | 8 | #if os(watchOS) 9 | import SwiftUI 10 | 11 | struct OtherAppRowView: View { 12 | private let otherApp: AKOtherApp 13 | 14 | @State private var appIconURL: String? 15 | 16 | init(_ otherApp: AKOtherApp) { 17 | self.otherApp = otherApp 18 | } 19 | 20 | var body: some View { 21 | Link(destination: otherApp.appStoreURL) { 22 | HStack(spacing: 12) { 23 | appIcon 24 | 25 | Text(otherApp.name) 26 | .lineLimit(2) 27 | } 28 | .padding(.vertical, 8) 29 | 30 | } 31 | .buttonStyle(.plain) 32 | .task { 33 | await loadAppIcon() 34 | } 35 | } 36 | 37 | private var appIcon: some View { 38 | ZStack { 39 | Color.secondary.opacity(0.2) 40 | 41 | if let appIcon = otherApp.appIcon { 42 | Image(uiImage: appIcon) 43 | .resizable() 44 | .scaledToFit() 45 | 46 | } else if let appIconURL = appIconURL { 47 | RemoteImageView(url: appIconURL) 48 | .scaledToFit() 49 | } 50 | } 51 | .frame(width: 32, height: 32) 52 | .clipShape(Circle()) 53 | .accessibilityHidden(true) 54 | } 55 | 56 | private func loadAppIcon() async { 57 | if otherApp.appIcon == nil { 58 | appIconURL = await AppIconNetworkManager.shared.fetchURL( 59 | for: otherApp 60 | ) 61 | } 62 | } 63 | } 64 | 65 | struct OtherAppRowView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | Form { 68 | OtherAppRowView(.example) 69 | } 70 | } 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /Sources/AboutKit/WatchTVAboutAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WatchTVAboutAppView.swift 3 | // AboutKit 4 | // 5 | // Created by Adam Foot on 14/03/2022. 6 | // 7 | 8 | #if os(watchOS) || os(tvOS) 9 | import SwiftUI 10 | 11 | /// A SwiftUI `View` which displays attributes and links relating to an app. 12 | public struct AboutAppView: View { 13 | 14 | /// A custom struct of type `AKConfiguration` containing details for AboutKit. 15 | private let configuration: AKConfiguration 16 | 17 | /// Initializes a new SwiftUI `View` which displays attributes and links relating to an app. 18 | /// - Parameter configuration: A custom struct of type `AKConfiguration` containing details for AboutKit. 19 | public init(configuration: AKConfiguration) { 20 | self.configuration = configuration 21 | } 22 | 23 | public var body: some View { 24 | Form { 25 | Section { 26 | HeaderView(app: configuration.app) 27 | .focusable() 28 | } 29 | 30 | if configuration.app.email != nil || configuration.app.websiteURL != nil { 31 | Section { 32 | if let email = configuration.app.email { 33 | ItemLabel( 34 | LocalizedStrings.email, 35 | details: email 36 | ) 37 | } 38 | 39 | if let websiteURL = configuration.app.websiteURL { 40 | ItemLabel( 41 | LocalizedStrings.website, 42 | details: websiteURL.absoluteString 43 | ) 44 | } 45 | } 46 | } 47 | 48 | if configuration.app.developer.profiles.isEmpty == false { 49 | Section { 50 | ForEach( 51 | Array(configuration.app.developer.profiles.enumerated()), 52 | id: \.0 53 | ) { _, profile in 54 | ItemLabel( 55 | profile.platform.name, 56 | details: profile.displayUsername 57 | ) 58 | } 59 | } 60 | } 61 | 62 | if configuration.app.profiles.isEmpty == false { 63 | Section { 64 | ForEach( 65 | Array(configuration.app.profiles.enumerated()), 66 | id: \.0 67 | ) { _, profile in 68 | ItemLabel( 69 | profile.platform.name, 70 | details: profile.displayUsername 71 | ) 72 | } 73 | } 74 | } 75 | 76 | if configuration.app.privacyPolicyURL != nil || configuration.app.termsOfUseURL != nil || configuration.app.acknowledgements?.frameworks?.isEmpty == false || configuration.app.acknowledgements?.people?.isEmpty == false { 77 | Section { 78 | if let privacyPolicyURL = configuration.app.privacyPolicyURL { 79 | ItemLabel( 80 | LocalizedStrings.privacyPolicy, 81 | details: privacyPolicyURL.absoluteString 82 | ) 83 | } 84 | 85 | if let termsOfUseURL = configuration.app.termsOfUseURL { 86 | ItemLabel( 87 | LocalizedStrings.termsOfUse, 88 | details: termsOfUseURL.absoluteString 89 | ) 90 | } 91 | 92 | if let acknowledgements = configuration.app.acknowledgements { 93 | if acknowledgements.frameworks?.isEmpty == false || acknowledgements.people?.isEmpty == false { 94 | NavigationLink(LocalizedStrings.acknowledgements) { 95 | AcknowledgementsView(acknowledgements) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | if let testFlightURL = configuration.app.testFlightURL { 103 | Section { 104 | ItemLabel( 105 | LocalizedStrings.testFlight, 106 | details: testFlightURL.absoluteString 107 | ) 108 | } 109 | } 110 | 111 | if configuration.otherApps.isEmpty == false { 112 | Section { 113 | ForEach( 114 | configuration.otherApps, 115 | content: OtherAppRowView.init 116 | ) 117 | 118 | Link( 119 | LocalizedStrings.viewAllApps, 120 | destination: configuration.app.developer.appStoreURL 121 | ) 122 | } header: { 123 | #if os(tvOS) 124 | SectionHeaderLabel(LocalizedStrings.otherApps) 125 | #else 126 | Text(LocalizedStrings.otherApps) 127 | #endif 128 | } 129 | } 130 | } 131 | .navigationTitle(LocalizedStrings.aboutApp) 132 | } 133 | } 134 | 135 | struct AboutAppView_Previews: PreviewProvider { 136 | static var previews: some View { 137 | NavigationView { 138 | AboutAppView(configuration: .example) 139 | } 140 | } 141 | } 142 | #endif 143 | -------------------------------------------------------------------------------- /Tests/AboutKitTests/AboutKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AboutKit 3 | 4 | final class AboutKitTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/AboutKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(AboutKitTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import AboutKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += AboutKitTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------