├── README.md
├── Example
└── simplecommon.example
│ ├── simplecommon.example
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon-thumb.imageset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── simplecommon_exampleApp.swift
│ ├── ColorTestView.swift
│ ├── SimpleShareSheetView.swift
│ ├── ContentView.swift
│ ├── AppIcon.swift
│ ├── TrackableScrollViewTestView.swift
│ ├── SimplePanelTestView.swift
│ └── MailTestView.swift
│ └── simplecommon.example.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── Tests
└── SimpleCommonTests
│ └── SimpleCommonTests.swift
├── Sources
└── SimpleCommon
│ ├── SimpleMappedCGFloatPreferenceKey.swift
│ ├── SimpleMappedCGSizePreferenceKey.swift
│ ├── Extensions
│ ├── UIColor+data.swift
│ ├── Array+RawRepresentable.swift
│ ├── UIViewController+dismissKeyboard.swift
│ ├── UIApplication+dismissKeyboard.swift
│ ├── Shape+stroke.swift
│ ├── UIColor+mix.swift
│ └── UIColor+name.swift
│ ├── SimplePanelStyle.swift
│ ├── SimpleShareSheetView.swift
│ ├── SimpleSafariActivity.swift
│ ├── SimpleCloudSettings.swift
│ ├── SimplePanel.swift
│ ├── SimpleMailView.swift
│ ├── SimpleScrollView.swift
│ ├── SimpleAppIcon.swift
│ └── SimpleIconLabel.swift
├── .github
└── workflows
│ ├── xcode.yml
│ └── documentation.yml
├── Package.swift
└── .gitignore
/README.md:
--------------------------------------------------------------------------------
1 | # SimpleCommon
2 |
3 | SwiftUI utilties and views that just feel native
4 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/SimpleCommonTests/SimpleCommonTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SimpleCommon
3 |
4 | final class SimpleCommonTests: XCTestCase {
5 | func testMainBundleHasBundleIdentifier() throws {
6 | XCTAssertNotNil(Bundle.main.bundleIdentifier)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/Assets.xcassets/AppIcon-thumb.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "settings_icon.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/simplecommon_exampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // simplecommon_exampleApp.swift
3 | // simplecommon.example
4 | //
5 | // Created by Zachary Gorak on 10/25/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct simplecommon_exampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleMappedCGFloatPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct SimpleMappedCGFloatPreferenceKey: PreferenceKey {
4 | public typealias Value = [String: CGFloat]
5 |
6 | public static var defaultValue: [String: CGFloat] = [:]
7 |
8 | public static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) {
9 | value.merge(nextValue()) { $1 }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleMappedCGSizePreferenceKey.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Foundation
4 |
5 | public struct SimpleMappedCGSizePreferenceKey: PreferenceKey {
6 | public typealias Value = [String: CGSize]
7 |
8 | public static var defaultValue: [String: CGSize] = [:]
9 |
10 | public static func reduce(value: inout [String: CGSize], nextValue: () -> [String: CGSize]) {
11 | value.merge(nextValue()) { $1 }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/UIColor+data.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public extension UIColor {
4 | var data: Data? {
5 | return try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
6 | }
7 |
8 | func from(data: Data) -> UIColor? {
9 | guard let color = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Self.self, from: data) else {
10 | return nil
11 | }
12 | return color
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-docc-plugin",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-docc-plugin.git",
7 | "state" : {
8 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
9 | "version" : "1.0.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/xcode.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: macos-12
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: List Schemes
17 | run: xcodebuild -showdestinations -scheme SimpleCommon
18 | - name: Build
19 | run: xcodebuild -scheme SimpleCommon -destination 'platform=macOS,variant=Mac Catalyst'
20 | - name: Run tests
21 | run: xcodebuild test-without-building -scheme SimpleCommon -destination 'platform=macOS,variant=Mac Catalyst'
22 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimplePanelStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A type that applies standard interaction behavior and a custom appearance to a ``SimplePanel``
4 | public enum SimplePanelStyle: Int, Codable, CaseIterable, Identifiable {
5 | /// A simple trailing close button
6 | case close
7 | /// A trailing save button
8 | case save
9 | /// A trailing cancel button
10 | case cancel
11 | /// A leading cancel and trailing save button
12 | case saveAndCancel
13 | /// A trailing done button
14 | case done
15 |
16 | public var id: Int {
17 | self.rawValue
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/ColorTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | import SimpleCommon
5 |
6 | struct ColorTestView: View {
7 | let customColor = UIColor.init(red: 158.0/255.0, green: 38.0/255.0, blue: 27.0/255.0, alpha: 1.0)
8 | @State var date = Date()
9 | var body: some View {
10 | List {
11 | Text("Custom color name: \(customColor.name ?? "Unknown")")
12 | .id(date)
13 | }
14 | .onAppear {
15 | UIColor.additionalNameMapping[customColor] = "Lobsters Red"
16 | date = Date()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/SimpleShareSheetView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import SimpleCommon
4 |
5 | struct ShareSheetTestView: View {
6 | @State var contents = "Something to share"
7 | @State var show = false
8 | var body: some View {
9 | TextField("Contents", text: $contents)
10 | Button {
11 | UIApplication.shared.dismissKeyboard()
12 | } label: {
13 | Text("Dismiss keyboard (UIApplication)")
14 | }
15 | Button(action: {show.toggle()}, label: {
16 | Text("Show")
17 | })
18 | .sheet(isPresented: $show) {
19 | SimpleShareSheetView(activityItems: [contents])
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/Array+RawRepresentable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// https://stackoverflow.com/a/65598711
4 | extension Array: RawRepresentable where Element: Codable {
5 | public init?(rawValue: String) {
6 | guard let data = rawValue.data(using: .utf8),
7 | let result = try? JSONDecoder().decode([Element].self, from: data)
8 | else {
9 | return nil
10 | }
11 | self = result
12 | }
13 |
14 | public var rawValue: String {
15 | guard let data = try? JSONEncoder().encode(self),
16 | let result = String(data: data, encoding: .utf8)
17 | else {
18 | return "[]"
19 | }
20 | return result
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/UIViewController+dismissKeyboard.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public extension UIViewController {
4 | /// Notifies this object that it has been asked to relinquish its status as first responder in its window and causes the view (or one of its embedded text fields) to resign the first responder status.
5 | ///
6 | /// - parameter force: Specify `true` to force the first responder to resign, regardless of whether it wants to do so.
7 | /// - returns: `true` if the view resigned the first responder status or `false` if it did not.
8 | @discardableResult
9 | @objc func dismissKeyboard(force: Bool = false) -> Bool {
10 | if !resignFirstResponder(), !force {
11 | return false
12 | }
13 | return view.endEditing(false)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/UIApplication+dismissKeyboard.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public extension UIApplication {
4 | /// - returns: `true` if a responder object handled the `UIResponder.resignFirstResponder` action message, `false` if no object in the responder chain handled the message.
5 | @discardableResult
6 | func dismissKeyboard() -> Bool {
7 | return sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
8 | // we could also do the following
9 | // connectedScenes
10 | // .filter {$0.activationState == .foregroundActive}
11 | // .map {$0 as? UIWindowScene}
12 | // .compactMap({$0})
13 | // .first?.windows
14 | // .filter {$0.isKeyWindow}
15 | // .first?.endEditing(true)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: Documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | jobs:
11 | build-and-deploy:
12 | runs-on: macos-12
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: Generate Documentation
17 | run: |
18 | mkdir docs && \
19 | xcodebuild build -scheme SimpleCommon -destination generic/platform=iOS && \
20 | xcodebuild docbuild -scheme SimpleCommon \
21 | -destination generic/platform=iOS \
22 | OTHER_DOCC_FLAGS="--transform-for-static-hosting --output-path docs --hosting-base-path SimpleCommon"
23 | - name: Deploy 🚀
24 | uses: JamesIves/github-pages-deploy-action@v4
25 | with:
26 | branch: gh-pages # The branch the action should deploy to.
27 | folder: docs # The folder the action should deploy.
28 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/Shape+stroke.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// https://www.hackingwithswift.com/quick-start/swiftui/how-to-fill-and-stroke-shapes-at-the-same-time
4 | public extension Shape {
5 | func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
6 | self
7 | .stroke(strokeStyle, lineWidth: lineWidth)
8 | .background(self.fill(fillStyle))
9 | }
10 | }
11 |
12 | /// https://www.hackingwithswift.com/quick-start/swiftui/how-to-fill-and-stroke-shapes-at-the-same-time
13 | public extension InsettableShape {
14 | func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
15 | self
16 | .strokeBorder(strokeStyle, lineWidth: lineWidth)
17 | .background(self.fill(fillStyle))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // simplecommon.example
4 | //
5 | // Created by Zachary Gorak on 10/25/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 |
12 | struct ContentView: View {
13 | var body: some View {
14 | NavigationView {
15 | List {
16 | NavigationLink("Test Mail", destination: MailTestView())
17 | NavigationLink("Test Share Sheet", destination: ShareSheetTestView())
18 | NavigationLink("Test TrackableScrollView", destination: TrackableScrollViewTestView())
19 | NavigationLink("Test SimplePanel", destination: SimplePanelTestView())
20 | NavigationLink("Test Color", destination: ColorTestView())
21 | NavigationLink("Test AppIcon", destination: AppIcon())
22 | }
23 | }
24 | }
25 | }
26 |
27 | struct ContentView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | ContentView()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/AppIcon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppIcon.swift
3 | // simplecommon.example
4 | //
5 | // Created by Zachary Gorak on 9/24/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import SimpleCommon
11 |
12 | extension SimpleAppIcon {
13 | static let `default` = SimpleAppIcon(alternateIconName: nil, assetName: "AppIcon-thumb")
14 | }
15 |
16 | struct AppIcon: View {
17 | @Environment(\.simpleAppIcon) var icon
18 |
19 | var body: some View {
20 | List {
21 | Section("Current") {
22 | HStack {
23 | Spacer()
24 | icon?.thumbnail()
25 | Spacer()
26 | }
27 | LabeledContent("alternativeIconName", value: "\(icon?.alternateIconName ?? "nil")")
28 | LabeledContent("assetnName", value: "\(icon?.assetName ?? "nil")")
29 | LabeledContent("bundle", value: "\(icon?.bundle?.bundleIdentifier ?? "nil")")
30 | }
31 | }
32 | .onAppear {
33 | SimpleAppIcon.allIcons.insert(.default)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/TrackableScrollViewTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import SimpleCommon
4 |
5 | struct TrackableScrollViewTestView: View {
6 | @State var offset: CGFloat = .zero
7 | @State var showIndicator = true
8 | @State var dontScrollIfFits = false
9 | @State var axis = Axis.Set.vertical.rawValue
10 | var body: some View {
11 | SimpleScrollView(
12 | Axis.Set(rawValue: axis),
13 | showIndicators: showIndicator,
14 | dontScrollIfContentFits: dontScrollIfFits,
15 | contentOffset: $offset
16 | ) {
17 | VStack {
18 | Toggle(isOn: $showIndicator, label: { Text("Show indicator") })
19 | Toggle(isOn: $dontScrollIfFits, label: { Text("Don't scroll if content fits")})
20 | Picker("Axis", selection: $axis) {
21 | Text("Horizontal")
22 | .tag(Axis.Set.horizontal.rawValue)
23 | Text("Vertical")
24 | .tag(Axis.Set.vertical.rawValue)
25 | }
26 | Text("\(offset)")
27 | }
28 | .padding()
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/UIColor+mix.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor.swift
3 | // claw
4 | //
5 | // Created by Zachary Gorak on 9/13/20.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import SwiftUI
11 |
12 | //https://stackoverflow.com/a/63003757/193772
13 | public extension UIColor {
14 | func mix(with color: UIColor, amount: CGFloat) -> Self {
15 | var red1: CGFloat = 0
16 | var green1: CGFloat = 0
17 | var blue1: CGFloat = 0
18 | var alpha1: CGFloat = 0
19 |
20 | var red2: CGFloat = 0
21 | var green2: CGFloat = 0
22 | var blue2: CGFloat = 0
23 | var alpha2: CGFloat = 0
24 |
25 | getRed(&red1, green: &green1, blue: &blue1, alpha: &alpha1)
26 | color.getRed(&red2, green: &green2, blue: &blue2, alpha: &alpha2)
27 |
28 | return Self(
29 | red: red1 * CGFloat(1.0 - amount) + red2 * amount,
30 | green: green1 * CGFloat(1.0 - amount) + green2 * amount,
31 | blue: blue1 * CGFloat(1.0 - amount) + blue2 * amount,
32 | alpha: alpha1
33 | )
34 | }
35 |
36 | func lighter(by amount: CGFloat = 0.2) -> Self { mix(with: .white, amount: amount) }
37 | func darker(by amount: CGFloat = 0.2) -> Self { mix(with: .black, amount: amount) }
38 | }
39 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/SimplePanelTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import SimpleCommon
4 |
5 | extension SimplePanelStyle: CustomStringConvertible {
6 | public var description: String {
7 | switch self {
8 | case .close:
9 | return "Close"
10 | case .cancel: return "Cancel"
11 | case .save: return "Save"
12 | case .saveAndCancel: return "Save and Cancel"
13 | case .done: return "Done"
14 | }
15 | }
16 | }
17 |
18 | struct SimplePanelTestView: View {
19 | @State var style = SimplePanelStyle.close
20 | @State var show = false
21 | var body: some View {
22 | VStack {
23 | Picker(selection: $style, content: {
24 | ForEach(SimplePanelStyle.allCases) { style in
25 | Text("\(style.description)")
26 | .tag(style)
27 | }
28 | }, label: {
29 | Text("\(style.description)")
30 | })
31 | Button {
32 | show.toggle()
33 | } label: {
34 | Text("Show")
35 | }
36 | }
37 | .sheet(isPresented: $show, content: {
38 | SimplePanel(style: style) {
39 | Text("Hello World!")
40 | }
41 | })
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
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: "SimpleCommon",
8 | platforms: [
9 | .iOS(.v15),
10 | .macOS(.v12),
11 | .macCatalyst(.v15),
12 | .tvOS(.v15)
13 | ],
14 | products: [
15 | // Products define the executables and libraries a package produces, and make them visible to other packages.
16 | .library(
17 | name: "SimpleCommon",
18 | targets: ["SimpleCommon"]),
19 | ],
20 | dependencies: [
21 | // Dependencies declare other packages that this package depends on.
22 | .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
23 | .package(url: "https://github.com/ggruen/CloudKitSyncMonitor.git", from: "1.0.0")
24 | ],
25 | targets: [
26 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
27 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
28 | .target(
29 | name: "SimpleCommon",
30 | dependencies: [
31 | .product(name: "CloudKitSyncMonitor", package: "CloudKitSyncMonitor")
32 | ]),
33 | .testTarget(
34 | name: "SimpleCommonTests",
35 | dependencies: ["SimpleCommon"]),
36 | ]
37 | )
38 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleShareSheetView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | #if !os(tvOS)
5 | public struct SimpleShareSheetView: UIViewControllerRepresentable {
6 | public typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
7 |
8 | let activityItems: [Any]
9 | let applicationActivities: [UIActivity]?
10 | let excludedActivityTypes: [UIActivity.ActivityType]?
11 | let callback: Callback?
12 |
13 | public init(
14 | activityItems: [Any],
15 | applicationActivities: [UIActivity]? = nil,
16 | excludedActivityTypes: [UIActivity.ActivityType]? = nil,
17 | callback: Callback? = nil
18 | ) {
19 | self.activityItems = activityItems
20 | self.applicationActivities = applicationActivities
21 | self.excludedActivityTypes = excludedActivityTypes
22 | self.callback = callback
23 | }
24 |
25 | public func makeUIViewController(context _: Context) -> UIActivityViewController {
26 | let controller = UIActivityViewController(
27 | activityItems: activityItems,
28 | applicationActivities: applicationActivities ?? []
29 | )
30 | controller.excludedActivityTypes = excludedActivityTypes
31 | controller.completionWithItemsHandler = callback
32 | return controller
33 | }
34 |
35 | public func updateUIViewController(_: UIActivityViewController, context _: Context) {
36 | // nothing to do here
37 | }
38 | }
39 | #endif
40 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleSafariActivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SafariActivity.swift
3 | // claw
4 | //
5 | // Created by Zachary Gorak on 9/22/20.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | #if !os(tvOS)
12 | /**
13 | An Open in Safari action for URLs
14 | */
15 | public class SimpleSafariActivity: UIActivity {
16 | public override var activityImage: UIImage? {
17 | let largeConfig = UIImage.SymbolConfiguration(scale: .large)
18 | return UIImage(systemName: "safari", withConfiguration: largeConfig)
19 | }
20 |
21 | open override var activityTitle: String? {
22 | return "Open in Default Browser"
23 | }
24 |
25 | public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
26 | for item in activityItems {
27 | if let url = item as? URL, UIApplication.shared.canOpenURL(url) {
28 | return true
29 | }
30 | }
31 | return false
32 | }
33 |
34 | var urls = [URL]()
35 |
36 | public override func prepare(withActivityItems activityItems: [Any]) {
37 | for item in activityItems {
38 | if let url = item as? URL, UIApplication.shared.canOpenURL(url) {
39 | urls.append(url)
40 | }
41 | }
42 | }
43 |
44 | public override func perform() {
45 | guard let url = urls.first else {
46 | self.activityDidFinish(false)
47 | return
48 | }
49 |
50 | UIApplication.shared.open(url, completionHandler: { status in
51 | self.activityDidFinish(status)
52 | })
53 | }
54 | }
55 | #endif
56 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/Extensions/UIColor+name.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | var additionalUIColorNameMap: [UIColor: String] = [:]
4 |
5 | public extension UIColor {
6 | static var additionalNameMapping: [UIColor: String] {
7 | get {
8 | return additionalUIColorNameMap
9 | }
10 | set {
11 | additionalUIColorNameMap = newValue
12 | }
13 | }
14 |
15 | /// Human readable name
16 | ///
17 | /// To add additional names use ``additionalNameMapping``
18 | var name: String? {
19 | switch self {
20 | case .systemIndigo:
21 | return "Indigo"
22 | case .systemCyan:
23 | return "Cyan"
24 | case .systemBrown:
25 | return "Brown"
26 | case .systemMint:
27 | return "Mint"
28 | case .systemPurple:
29 | return "Purple"
30 | case .systemOrange:
31 | return "Orange"
32 | case .systemTeal:
33 | return "Teal"
34 | case .systemPink:
35 | return "Pink"
36 | case .systemBlue:
37 | return "Blue"
38 | case .systemRed:
39 | return "Red"
40 | case .systemGray:
41 | return "Gray"
42 | case .systemGreen:
43 | return "Green"
44 | case .systemYellow:
45 | return "Yellow"
46 | case .white:
47 | return "White"
48 | case .black:
49 | return "Black"
50 | case .label:
51 | return "Primary"
52 | case .secondaryLabel:
53 | return "Secondary"
54 | case .clear:
55 | return "Clear"
56 | default:
57 | return UIColor.additionalNameMapping[self] ?? nil
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
11 | ## User settings
12 | xcuserdata/
13 |
14 | ## Xcode 8 and earlier
15 | *.xcscmblueprint
16 | *.xccheckout
17 |
18 | # Xcode
19 | #
20 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
21 |
22 | ## User settings
23 | xcuserdata/
24 |
25 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
26 | *.xcscmblueprint
27 | *.xccheckout
28 |
29 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
30 | build/
31 | DerivedData/
32 | *.moved-aside
33 | *.pbxuser
34 | !default.pbxuser
35 | *.mode1v3
36 | !default.mode1v3
37 | *.mode2v3
38 | !default.mode2v3
39 | *.perspectivev3
40 | !default.perspectivev3
41 |
42 | ## Obj-C/Swift specific
43 | *.hmap
44 |
45 | ## App packaging
46 | *.ipa
47 | *.dSYM.zip
48 | *.dSYM
49 |
50 | ## Playgrounds
51 | timeline.xctimeline
52 | playground.xcworkspace
53 |
54 | # Swift Package Manager
55 | #
56 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
57 | # Packages/
58 | # Package.pins
59 | # Package.resolved
60 | # *.xcodeproj
61 | #
62 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
63 | # hence it is not needed unless you have added a package configuration file to your project
64 | # .swiftpm
65 |
66 | .build/
67 |
68 | # CocoaPods
69 | #
70 | # We recommend against adding the Pods directory to your .gitignore. However
71 | # you should judge for yourself, the pros and cons are mentioned at:
72 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
73 | #
74 | # Pods/
75 | #
76 | # Add this line if you want to avoid checking in source code from the Xcode workspace
77 | # *.xcworkspace
78 |
79 | # Carthage
80 | #
81 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
82 | # Carthage/Checkouts
83 |
84 | Carthage/Build/
85 |
86 | # Accio dependency management
87 | Dependencies/
88 | .accio/
89 |
90 | # fastlane
91 | #
92 | # It is recommended to not store the screenshots in the git repo.
93 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
94 | # For more information about the recommended setup visit:
95 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
96 |
97 | fastlane/report.xml
98 | fastlane/Preview.html
99 | fastlane/screenshots/**/*.png
100 | fastlane/test_output
101 |
102 | # Code Injection
103 | #
104 | # After new code Injection tools there's a generated folder /iOSInjectionProject
105 | # https://github.com/johnno1962/injectionforxcode
106 |
107 | iOSInjectionProject/
108 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example/MailTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import MessageUI
3 |
4 | import SimpleCommon
5 |
6 | struct MailTestView: View {
7 | @State var showSheet: Bool = false
8 | @State var showFullScreenCover: Bool = false
9 | @State var show = false
10 | @State var subject = "Test Subject"
11 | @State var result: Result?
12 |
13 | var composeView: some View {
14 | SimpleMailView(
15 | result: $result,
16 | subject: subject,
17 | toReceipt: ["to@me.com", "to@you.com"]
18 | )
19 | }
20 |
21 | var body: some View {
22 | VStack {
23 | #if targetEnvironment(simulator)
24 | Text("This will not work in the simulator")
25 | #endif
26 | if !MFMailComposeViewController.canSendMail() {
27 | Text("Unable to send mail!")
28 | }
29 | Spacer()
30 | TextField("Subject", text: $subject)
31 | Button {
32 | if MFMailComposeViewController.canSendMail() {
33 | showSheet.toggle()
34 | }
35 | } label: {
36 | Text("Show Sheet")
37 | }
38 | Button {
39 | if MFMailComposeViewController.canSendMail() {
40 | showFullScreenCover.toggle()
41 | }
42 | } label: {
43 | Text("Show FullScreenCover")
44 | }
45 | Button {
46 | show.toggle()
47 | } label: {
48 | Text("Show")
49 | }
50 | Spacer()
51 | HStack {
52 | Text("Result:")
53 | switch result {
54 | case .none:
55 | Text("None")
56 | case .some(let wrapped):
57 | switch wrapped {
58 | case .failure(let error):
59 | Text("Error: \(error.localizedDescription)")
60 | case .success(let composeResult):
61 | Text("Success")
62 | switch composeResult {
63 | case .cancelled:
64 | Text("Cancelled")
65 | case .failed:
66 | Text("Failed")
67 | case .saved:
68 | Text("Saved")
69 | case .sent:
70 | Text("Sent")
71 | @unknown default:
72 | Text("Unknown")
73 | }
74 | }
75 | }
76 | }
77 | }
78 | .sheet(isPresented: $showSheet, content: {
79 | composeView
80 | })
81 | .fullScreenCover(isPresented: $showFullScreenCover) {
82 | composeView
83 | }
84 | .composeMail(isPresented: $show, result: $result, subject: subject)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleCloudSettings.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | #if canImport(CloudKitSyncMonitor)
3 | import CloudKitSyncMonitor
4 |
5 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
6 | public struct SimpleCloudSettings: View {
7 | @ObservedObject var syncMonitor = SyncMonitor.shared
8 |
9 | /// Create a simple iCloud settings view
10 | /// - Parameter syncMonitor: If `nil`, use the shared instance
11 | public init(syncMonitor: SyncMonitor? = nil) {
12 | if let syncMonitor {
13 | self.syncMonitor = syncMonitor
14 | } else {
15 | self.syncMonitor = .shared
16 | }
17 | }
18 |
19 | public var body: some View {
20 | List {
21 | LabeledContent {
22 | Text(stateText(for: syncMonitor.setupState))
23 | } label: {
24 | Text("Setup State")
25 | }
26 | LabeledContent {
27 | Text(stateText(for: syncMonitor.exportState))
28 | } label: {
29 | Text("Export State")
30 | }
31 |
32 | LabeledContent {
33 | Text(stateText(for: syncMonitor.importState))
34 | } label: {
35 | Text("Import State")
36 | }
37 |
38 | Section("Errors") {
39 | if hasError {
40 | if syncMonitor.notSyncing {
41 | Text("Sync should be working, but isn't. Look for a badge on Settings or other possible issues.")
42 | }
43 | if syncMonitor.syncError {
44 | if let e = syncMonitor.setupError {
45 | Text("Unable to set up iCloud sync, changes won't be saved! \(e.localizedDescription)")
46 | }
47 | if let e = syncMonitor.importError {
48 | Text("Import is broken: \(e.localizedDescription)")
49 | }
50 | if let e = syncMonitor.exportError {
51 | Text("Export is broken - your changes aren't being saved! \(e.localizedDescription)")
52 | }
53 | }
54 | } else {
55 | Text("No errors detected")
56 | }
57 | }
58 | }
59 | .navigationBarTitle("iCloud Status")
60 | }
61 |
62 | var hasError: Bool {
63 | syncMonitor.syncError || syncMonitor.notSyncing
64 | }
65 |
66 | fileprivate var dateFormatter: DateFormatter = {
67 | let dateFormatter = DateFormatter()
68 | dateFormatter.dateStyle = DateFormatter.Style.short
69 | dateFormatter.timeStyle = DateFormatter.Style.short
70 | return dateFormatter
71 | }()
72 |
73 | /// Returns a user-displayable text description of the sync state
74 | func stateText(for state: SyncMonitor.SyncState) -> String {
75 | switch state {
76 | case .notStarted:
77 | return "Not started"
78 | case .inProgress(started: let date):
79 | return "In progress since \(dateFormatter.string(from: date))"
80 | case let .succeeded(started: _, ended: endDate):
81 | return "Suceeded at \(dateFormatter.string(from: endDate))"
82 | case let .failed(started: _, ended: endDate, error: _):
83 | return "Failed at \(dateFormatter.string(from: endDate))"
84 | }
85 | }
86 | }
87 |
88 | #endif
89 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimplePanel.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | /// A wrapper view for presenting a view with pre-defined NavigationView leading and trailing items
5 | ///
6 | /// Use ``SimplePanel`` to wrap a view inside a ``NavigationView`` with pre-defined
7 | /// leading and trailing items. See ``SimplePanelStyle`` for a list of available styles.
8 | public struct SimplePanel: View where Content: View {
9 | let style: SimplePanelStyle
10 | let leadingAction: (() async throws -> Void)?
11 | let trailingAction: (() async throws -> Void)?
12 | let content: () -> Content
13 |
14 | @Environment(\.dismiss) private var dismiss
15 |
16 | /// A content wrapper useful for sheets
17 | /// - Parameters:
18 | /// - style: The panel's style
19 | /// - leadingAction: The action for the leading navigation bar item
20 | /// - trailingAction: The action for the trailing navigation bar item
21 | /// - content: The content to show in the panel
22 | ///
23 | /// If the action does not throw then the view will be dismissed
24 | public init(
25 | style: SimplePanelStyle = .close,
26 | leadingAction: ( () async throws -> Void)? = nil,
27 | trailingAction: (() async throws -> Void)? = nil,
28 | @ViewBuilder content: @escaping () -> Content
29 | ) {
30 | self.content = content
31 | self.style = style
32 | self.leadingAction = leadingAction
33 | self.trailingAction = trailingAction
34 | }
35 |
36 | @ViewBuilder var trailingItem: some View {
37 | switch style {
38 | case .close:
39 | Image(systemName: "xmark.circle.fill")
40 | #if !os(tvOS)
41 | .foregroundColor(Color(UIColor.systemGray3))
42 | #endif
43 | case .save:
44 | Text("Save")
45 | case .saveAndCancel:
46 | Text("Save")
47 | .bold()
48 | case .cancel:
49 | Text("Cancel")
50 | case .done:
51 | Text("Done")
52 | }
53 | }
54 |
55 | @ViewBuilder var leadingItem: some View {
56 | switch style {
57 | case .saveAndCancel:
58 | Text("Cancel")
59 | default:
60 | EmptyView()
61 | }
62 | }
63 |
64 | @ViewBuilder var leadingButton: some View {
65 | Button(role: .cancel, action: {
66 | Task {
67 | do {
68 | try await self.leadingAction?()
69 | dismiss()
70 | } catch {
71 | // no-op
72 | }
73 | }
74 | }, label: {
75 | leadingItem
76 | })
77 | }
78 |
79 | @ViewBuilder var trailingButton: some View {
80 | switch style {
81 | case .cancel:
82 | Button(role: .cancel) {
83 | doTrailingAction()
84 | } label: {
85 | trailingItem
86 | }
87 | case .close:
88 | Button {
89 | doTrailingAction()
90 | } label: {
91 | trailingItem
92 | }
93 | #if os(tvOS)
94 | .buttonStyle(.card)
95 | #endif
96 | default:
97 | Button {
98 | doTrailingAction()
99 | } label: {
100 | trailingItem
101 | }
102 | }
103 | }
104 |
105 | func doTrailingAction() {
106 | Task {
107 | do {
108 | try await trailingAction?()
109 | dismiss()
110 | } catch {
111 | // no-op
112 | }
113 | }
114 | }
115 |
116 | var hasLeading: Bool {
117 | style == .saveAndCancel
118 | }
119 |
120 | public var body: some View {
121 | NavigationView {
122 | if hasLeading {
123 | content()
124 | .navigationBarItems(
125 | leading: leadingButton,
126 | trailing: trailingButton
127 | )
128 | } else {
129 | content()
130 | .navigationBarItems(
131 | trailing: trailingButton
132 | )
133 | }
134 | }
135 | }
136 | }
137 |
138 | struct SwiftUIView_Previews: PreviewProvider {
139 | static var previews: some View {
140 | ForEach(SimplePanelStyle.allCases) { style in
141 | SimplePanel(style: style) {
142 | Text("Hello World")
143 | }
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleMailView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | #if canImport(MessageUI)
3 | import MessageUI
4 |
5 | /// A SwiftUI Wrapper for `MFMailComposeViewController`
6 | ///
7 | /// - SeeAlso: https://stackoverflow.com/a/56785754/193772
8 | public struct SimpleMailView: UIViewControllerRepresentable {
9 |
10 | @Binding var result: Result?
11 | @Environment(\.dismiss) private var dismiss
12 | var subject: String
13 | var toReceipt: [String]?
14 |
15 | public init(
16 | result: Binding?>,
17 | subject: String,
18 | toReceipt: [String]? = nil
19 | ) {
20 | self._result = result
21 | self.subject = subject
22 | self.toReceipt = toReceipt
23 | }
24 |
25 | public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
26 | @Binding var result: Result?
27 | var dismiss: DismissAction
28 |
29 |
30 | init(result: Binding?>,
31 | dismiss: DismissAction) {
32 | _result = result
33 | self.dismiss = dismiss
34 | }
35 |
36 | public func mailComposeController(_ controller: MFMailComposeViewController,
37 | didFinishWith result: MFMailComposeResult,
38 | error: Error?) {
39 | defer {
40 | DispatchQueue.main.async { [self] in
41 | dismiss()
42 | }
43 | }
44 | guard error == nil else {
45 | self.result = .failure(error!)
46 | return
47 | }
48 | self.result = .success(result)
49 | }
50 | }
51 |
52 | public func makeCoordinator() -> Coordinator {
53 | return Coordinator(result: $result, dismiss: dismiss)
54 | }
55 |
56 | public func makeUIViewController(context: UIViewControllerRepresentableContext) -> MFMailComposeViewController {
57 | let vc = MFMailComposeViewController()
58 | vc.mailComposeDelegate = context.coordinator
59 | vc.setToRecipients(toReceipt)
60 | vc.setSubject(subject)
61 | // Set CC, BCC, Body, Attachements
62 | return vc
63 | }
64 |
65 | public func updateUIViewController(_ uiViewController: MFMailComposeViewController,
66 | context: UIViewControllerRepresentableContext) {
67 | // no-op
68 | }
69 | }
70 |
71 | struct MailComposeViewModifier: ViewModifier {
72 | @Binding var isPresented: Bool
73 | @Binding var result: Result?
74 |
75 | var subject: String
76 | var recipients: [String]
77 | var onDismiss: (() -> Void)?
78 |
79 | func body(content: Content) -> some View {
80 | content
81 | .sheet(isPresented: .init(get: {
82 | isPresented && MFMailComposeViewController.canSendMail()
83 | }, set: {
84 | isPresented = $0
85 | }), onDismiss: {
86 | onDismiss?()
87 | }) {
88 | SimpleMailView(result: $result, subject: subject, toReceipt: recipients)
89 | }
90 | .onChange(of: isPresented) { value in
91 | if value, !MFMailComposeViewController.canSendMail() {
92 | let error = NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
93 | result = .failure(error)
94 | }
95 | }
96 | }
97 | }
98 |
99 | public extension View {
100 | /// Presents a mail compose sheet when a binding to a Boolean value that you provide is true.
101 | ///
102 | /// - parameters:
103 | /// - isPresented: A binding to a Boolean value that determines whether to present the sheet that you create in the modifier’s content closure.
104 | /// - result: A binding to a Result value that provides the finished result from the mail compose controller
105 | /// - onDismiss: The closure to execute when dismissing the sheet.
106 | /// - subject: The subject of the email
107 | /// - recipients: The recipients of the email
108 | func composeMail(
109 | isPresented: Binding,
110 | result: Binding?>,
111 | onDismiss: (() -> Void)? = nil,
112 | subject: String,
113 | recipients: [String]? = []) -> some View {
114 | self.modifier(
115 | MailComposeViewModifier(
116 | isPresented: isPresented,
117 | result: result,
118 | subject: subject,
119 | recipients: recipients ?? [],
120 | onDismiss: onDismiss
121 | )
122 | )
123 | }
124 | }
125 |
126 | #endif
127 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleScrollView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Track the scroll offset
4 | ///
5 | /// - SeeAlso: https://github.com/maxnatchanon/trackable-scroll-view
6 | /// - SeeAlso: https://github.com/dkk/ScrollViewIfNeeded
7 | public struct SimpleScrollView: View where Content: View {
8 | let axes: Axis.Set
9 | let showIndicators: Bool
10 | let dontScrollIfContentFits: Bool
11 | @Binding var contentOffset: CGFloat
12 | let content: () -> Content
13 | let preferenceKey: String
14 |
15 | @State var scrollViewSize: CGSize = .zero
16 | @State var contentSize: CGSize = .zero
17 |
18 | var scrollViewSizePreferenceKey: String {
19 | preferenceKey + ".scrollViewSize"
20 | }
21 | var contentSizePreferenceKey: String {
22 | preferenceKey + ".contentSize"
23 | }
24 | var offsetPreferenceKey: String {
25 | preferenceKey + ".offset"
26 | }
27 |
28 | /// Creates a new instance that’s scrollable in the direction of the given axis and can show indicators while scrolling.
29 | /// - Parameters:
30 | /// - axes: The scrollable axes of the scroll view.
31 | /// - showIndicators: A value that indicates whether the scroll view displays the scrollable component of the content offset, in a way that’s suitable for the platform.
32 | /// - preferenceKey: The unique base identifier for the preference keys used in this view
33 | /// - dontScrollIfContentFits: A Boolean value indicating if scrolling should be disabled if the content fits inside the ScrollView
34 | /// - contentOffset: A Binding to the content's offset value
35 | /// - content: The scroll view’s content.
36 | public init(
37 | _ axes: Axis.Set = .vertical,
38 | showIndicators: Bool = true,
39 | id preferenceKey: String = "TrackableScrollViewPreferenceKey",
40 | dontScrollIfContentFits: Bool = false,
41 | contentOffset: Binding,
42 | @ViewBuilder content: @escaping () -> Content
43 | ) {
44 | self.axes = axes
45 | self.showIndicators = showIndicators
46 | self._contentOffset = contentOffset
47 | self.content = content
48 | self.preferenceKey = preferenceKey
49 | self.dontScrollIfContentFits = dontScrollIfContentFits
50 | }
51 |
52 | var calculatedAxes: Axis.Set {
53 | if dontScrollIfContentFits, self.axes != [] {
54 | if axes == .vertical {
55 | return contentSize.height <= scrollViewSize.height ? [] : .vertical
56 | } else {
57 | return contentSize.width <= scrollViewSize.width ? [] : .horizontal
58 | }
59 | }
60 |
61 | return self.axes
62 | }
63 |
64 | public var body: some View {
65 | GeometryReader { outsideProxy in
66 | ScrollView(calculatedAxes, showsIndicators: self.showIndicators) {
67 | ZStack(alignment: self.axes == .vertical ? .top : .leading) {
68 | GeometryReader { insideProxy in
69 | Color.clear
70 | .preference(key: SimpleMappedCGFloatPreferenceKey.self, value: [self.offsetPreferenceKey: calculateContentOffset(fromOutsideProxy: outsideProxy, insideProxy: insideProxy)])
71 | }
72 | content()
73 | .background {
74 | GeometryReader { contentSizeProxy in
75 | Color.clear
76 | .preference(key: SimpleMappedCGSizePreferenceKey.self, value: [contentSizePreferenceKey: contentSizeProxy.size])
77 | }
78 | }
79 | }
80 | }
81 | .background {
82 | GeometryReader { scrollViewSizeProxy in
83 | Color.clear
84 | .preference(key: SimpleMappedCGSizePreferenceKey.self, value: [scrollViewSizePreferenceKey: scrollViewSizeProxy.size])
85 | }
86 | }
87 | }
88 | .onPreferenceChange(SimpleMappedCGFloatPreferenceKey.self) { value in
89 | contentOffset = value[offsetPreferenceKey] ?? .zero
90 | }
91 | .onPreferenceChange(SimpleMappedCGSizePreferenceKey.self) { value in
92 | self.scrollViewSize = value[scrollViewSizePreferenceKey] ?? .zero
93 | self.contentSize = value[contentSizePreferenceKey] ?? .zero
94 | }
95 | }
96 |
97 | private func calculateContentOffset(fromOutsideProxy outsideProxy: GeometryProxy, insideProxy: GeometryProxy) -> CGFloat {
98 | if axes == .vertical {
99 | return outsideProxy.frame(in: .global).minY - insideProxy.frame(in: .global).minY
100 | } else {
101 | return outsideProxy.frame(in: .global).minX - insideProxy.frame(in: .global).minX
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleAppIcon.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct SimpleAppIcon: Sendable, Codable, Equatable, Hashable {
4 | /// The name of the icon the system displays for the app.
5 | /// - SeeAlso: ``UIApplication.shared.alternateIconName``
6 | public let alternateIconName: String?
7 | /// The name of the image resource to lookup, as well as the
8 | /// localization key with which to label the image.
9 | public let assetName: String
10 | /// The bundle to search for the image resource and localization
11 | /// content. If `nil`, SwiftUI uses the main `Bundle`. Defaults to `nil`.
12 | public let bundle: Bundle?
13 |
14 | enum Keys: CodingKey {
15 | case alternativeIconName
16 | case assetName
17 | case bundleIdentifier
18 | case bundlePath
19 | case bundleURL
20 | case classNameForBundle
21 | }
22 |
23 | public init(from decoder: Decoder) throws {
24 | let container = try decoder.container(keyedBy: Keys.self)
25 | self.alternateIconName = try container.decode(String?.self, forKey: .alternativeIconName)
26 | self.assetName = try container.decode(String.self, forKey: .assetName)
27 | if let bundleIdentifier = try container.decodeIfPresent(String.self, forKey: .bundleIdentifier) {
28 | self.bundle = Bundle(identifier: bundleIdentifier)
29 | } else if let path = try container.decodeIfPresent(String.self, forKey: .bundlePath) {
30 | self.bundle = Bundle(path: path)
31 | } else if let url = try container.decodeIfPresent(URL.self, forKey: .bundleURL) {
32 | self.bundle = Bundle(url: url)
33 | } else if let className = try container.decodeIfPresent(String.self, forKey: .classNameForBundle), let anyClass = NSClassFromString(className) {
34 | self.bundle = Bundle(for: anyClass)
35 | } else {
36 | self.bundle = nil
37 | }
38 | }
39 |
40 | public func encode(to encoder: Encoder) throws {
41 | var container = encoder.container(keyedBy: Keys.self)
42 | try container.encode(alternateIconName, forKey: .alternativeIconName)
43 | try container.encode(assetName, forKey: .assetName)
44 | if let bundle, bundle != .main {
45 | if let bundleIdentifier = bundle.bundleIdentifier {
46 | try container.encode(bundleIdentifier, forKey: .bundleIdentifier)
47 | }
48 | try container.encode(bundle.bundlePath, forKey: .bundlePath)
49 | try container.encode(bundle.bundleURL, forKey: .bundleURL)
50 | if let principalClass = bundle.principalClass {
51 | let cString = class_getName(principalClass)
52 | let name = String(cString: cString)
53 | if !name.isEmpty {
54 | try container.encode(name, forKey: .classNameForBundle)
55 | }
56 | }
57 | }
58 | }
59 |
60 | public init(alternateIconName: String?, assetName: String, bundle: Bundle? = nil) {
61 | self.alternateIconName = alternateIconName
62 | self.assetName = assetName
63 | self.bundle = bundle
64 | }
65 |
66 | public func thumbnail(size: CGFloat = 64) -> some View {
67 | image
68 | .resizable()
69 | .frame(width: size, height: size)
70 | .mask(
71 | Image(systemName: "app.fill")
72 | .resizable()
73 | .aspectRatio(contentMode: .fit)
74 | )
75 | }
76 |
77 | public var image: Image {
78 | Image(assetName, bundle: bundle)
79 | }
80 |
81 | static public var allIcons = Set()
82 |
83 | static public var current: SimpleAppIcon? {
84 | allIcons.filter {
85 | $0.alternateIconName == UIApplication.shared.alternateIconName
86 | }.first
87 | }
88 |
89 | }
90 |
91 | @MainActor
92 | public class AppIconModel: ObservableObject {
93 | @Published public private(set) var icon = SimpleAppIcon.current
94 |
95 | public enum Errors: Error {
96 | case alternativeIconsNotSupported
97 | }
98 |
99 | public func set(_ icon: SimpleAppIcon) async throws {
100 | guard UIApplication.shared.supportsAlternateIcons else {
101 | throw Errors.alternativeIconsNotSupported
102 | }
103 | try await withCheckedThrowingContinuation { continuation in
104 | UIApplication.shared.setAlternateIconName(icon.alternateIconName) { error in
105 | if error == nil {
106 | self.icon = icon
107 | continuation.resume()
108 | } else {
109 | self.reset()
110 | continuation.resume(throwing: error!)
111 | }
112 | }
113 | }
114 | }
115 |
116 | func reset() {
117 | self.icon = SimpleAppIcon.current
118 | }
119 |
120 | func badSet(_ icon: SimpleAppIcon?) {
121 | guard let icon else {
122 | return
123 | }
124 | Task {
125 | try await set(icon)
126 | }
127 | }
128 |
129 | }
130 |
131 | private struct IconEnvironmentKey: EnvironmentKey {
132 | static let defaultValue: AppIconModel? = nil
133 | }
134 |
135 | extension EnvironmentValues {
136 | @MainActor public var simpleAppIcon: SimpleAppIcon? {
137 | get { self[IconEnvironmentKey.self]?.icon ?? AppIconModel().icon }
138 | set {
139 | self[IconEnvironmentKey.self]?.badSet(newValue)
140 | }
141 | }
142 |
143 | @MainActor public var simpleAppIconModel: AppIconModel {
144 | get { self[IconEnvironmentKey.self] ?? AppIconModel() }
145 | set {
146 | self[IconEnvironmentKey.self] = newValue
147 | }
148 | }
149 |
150 | }
151 |
--------------------------------------------------------------------------------
/Sources/SimpleCommon/SimpleIconLabel.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct HorizontallyAlignedLabelStyle: LabelStyle {
4 | ///https://www.hackingwithswift.com/forums/swiftui/vertical-align-icon-of-label/3346
5 | @Environment(\.sizeCategory) var size
6 |
7 | var style: any LabelStyle
8 |
9 | func makeBody(configuration: Configuration) -> some View {
10 | HStack(alignment: .center) {
11 | if size >= .accessibilityMedium {
12 | configuration.icon
13 | .frame(width: 80)
14 | } else {
15 | configuration.icon
16 | .frame(width: 30)
17 | }
18 | if !(style is IconOnlyLabelStyle) {
19 | configuration.title
20 | }
21 | }
22 | }
23 | }
24 |
25 | /// A standard label for user interface items, consisting of an app icon with a title.
26 | ///
27 | /// This user interface component is very similiar to the labels used in Settings.app with
28 | /// an application-like icon next to the label text
29 | ///
30 | /// The icon will be searched for in the following order:
31 | /// 1. `image`
32 | /// 2. `imagePath`
33 | /// 3. `imageName`
34 | /// 4. `systemImage`
35 | public struct SimpleIconLabel: View {
36 | let iconBackgroundColor: Color
37 | let iconColor: Color
38 | let systemImage: String?
39 | let image: Image?
40 | let imageName: String?
41 | let imageFile: String?
42 | let text: S
43 | let iconScale: CGFloat
44 | let view: () -> Content
45 | var labelStyle: any LabelStyle = TitleAndIconLabelStyle()
46 |
47 | /// Creates a label with an icon image and a title generated from a string.
48 | /// - Parameters:
49 | /// - iconBackgroundColor: The icon's background color
50 | /// - iconColor: The icon's foreground color
51 | /// - systemImage: The system image name to use for the icon
52 | /// - image: The image for the icon
53 | /// - imageName: The image name for the icon
54 | /// - imagePath: The image file path for the icon
55 | /// - text: The label's text
56 | /// - iconScale: The scale of the icon
57 | public init(
58 | iconBackgroundColor: Color = Color.accentColor,
59 | iconColor: Color = Color.white,
60 | systemImage: String? = nil,
61 | image: Image? = nil,
62 | imageName: String? = nil,
63 | imagePath: String? = nil,
64 | text: S,
65 | iconScale: Double = 0.6,
66 | @ViewBuilder view: @escaping () -> Content = { Color.clear }
67 | ) {
68 | self.iconBackgroundColor = iconBackgroundColor
69 | self.iconColor = iconColor
70 | self.systemImage = systemImage
71 | self.image = image
72 | self.imageName = imageName
73 | self.imageFile = imagePath
74 | self.text = text
75 | self.iconScale = iconScale
76 | self.view = view
77 | }
78 |
79 | public var body: some View {
80 | Label(
81 | title: {
82 | Text(text)
83 | .foregroundColor(Color(UIColor.label))
84 | },
85 | icon: {
86 | Image(systemName: "app.fill")
87 | .resizable()
88 | .aspectRatio(contentMode: .fit)
89 | .foregroundColor(self.iconBackgroundColor)
90 | .overlay {
91 | if let image = image {
92 | modified(image: image)
93 | }
94 | else if let path = imageFile, let uiImage = UIImage(contentsOfFile: path) {
95 | modified(image: Image(uiImage: uiImage))
96 | }
97 | else if let name = imageName {
98 | modified(image: Image(name))
99 | } else if let systemImage {
100 | modified(image: Image(systemName: systemImage))
101 | } else {
102 | view()
103 | .foregroundColor(iconColor)
104 | .scaleEffect(CGSize(width: iconScale, height: iconScale))
105 | }
106 | }
107 | .mask {
108 | Image(systemName: "app.fill")
109 | .resizable()
110 | .aspectRatio(contentMode: .fit)
111 | .foregroundColor(.black)
112 | }
113 | }
114 | )
115 | .labelStyle(HorizontallyAlignedLabelStyle(style: labelStyle))
116 | }
117 |
118 | func modified(image: Image) -> some View {
119 | image
120 | .resizable()
121 | .aspectRatio(contentMode: .fit)
122 | .scaleEffect(CGSize(width: iconScale, height: iconScale))
123 | .foregroundColor(self.iconColor)
124 | }
125 |
126 | /// Hides the title of this view.
127 | public func labelsHidden() -> Self {
128 | var _self = self
129 | _self.labelStyle = IconOnlyLabelStyle()
130 | return _self
131 | }
132 | }
133 |
134 | struct IconLabel_Previews: PreviewProvider {
135 | static var previews: some View {
136 | VStack(alignment: .leading) {
137 | SimpleIconLabel(text: "Hello")
138 | SimpleIconLabel(iconBackgroundColor: .blue, iconColor: .white, systemImage: "square", image: nil, text: "Hello Square @0.6", iconScale: 0.6)
139 | SimpleIconLabel(iconBackgroundColor: .blue, iconColor: .red, systemImage: "checkmark", image: nil, text: "Hello Checkmark @1.0", iconScale: 1.0)
140 | SimpleIconLabel(iconBackgroundColor: .black, text: "Twitter", iconScale: 1.0) {
141 | Text("𝕏")
142 | }
143 | SimpleIconLabel(systemImage: "eye.slash", text: "Hidden")
144 | .labelsHidden()
145 | }
146 | .previewLayout(.sizeThatFits)
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Example/simplecommon.example/simplecommon.example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | F63BFF3129087E9B00E18B62 /* simplecommon_exampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF3029087E9B00E18B62 /* simplecommon_exampleApp.swift */; };
11 | F63BFF3329087E9B00E18B62 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF3229087E9B00E18B62 /* ContentView.swift */; };
12 | F63BFF3529087E9D00E18B62 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F63BFF3429087E9D00E18B62 /* Assets.xcassets */; };
13 | F63BFF3829087E9D00E18B62 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F63BFF3729087E9D00E18B62 /* Preview Assets.xcassets */; };
14 | F63BFF4129087F1B00E18B62 /* MailTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF4029087F1B00E18B62 /* MailTestView.swift */; };
15 | F63BFF4429087F5D00E18B62 /* SimpleCommon in Frameworks */ = {isa = PBXBuildFile; productRef = F63BFF4329087F5D00E18B62 /* SimpleCommon */; };
16 | F63BFF482908B3D000E18B62 /* TrackableScrollViewTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF472908B3D000E18B62 /* TrackableScrollViewTestView.swift */; };
17 | F63BFF4A29099D7700E18B62 /* SimplePanelTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF4929099D7700E18B62 /* SimplePanelTestView.swift */; };
18 | F63BFF4C2909A33E00E18B62 /* ColorTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF4B2909A33E00E18B62 /* ColorTestView.swift */; };
19 | F63BFF54290A391800E18B62 /* SimpleShareSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BFF53290A391800E18B62 /* SimpleShareSheetView.swift */; };
20 | F6A6B5DE2AC0EAE7001795A8 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A6B5DD2AC0EAE7001795A8 /* AppIcon.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXFileReference section */
24 | F63BFF2D29087E9B00E18B62 /* simplecommon.example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = simplecommon.example.app; sourceTree = BUILT_PRODUCTS_DIR; };
25 | F63BFF3029087E9B00E18B62 /* simplecommon_exampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = simplecommon_exampleApp.swift; sourceTree = ""; };
26 | F63BFF3229087E9B00E18B62 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | F63BFF3429087E9D00E18B62 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
28 | F63BFF3729087E9D00E18B62 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
29 | F63BFF3F29087EB500E18B62 /* SimpleCommon */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SimpleCommon; path = ../..; sourceTree = ""; };
30 | F63BFF4029087F1B00E18B62 /* MailTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailTestView.swift; sourceTree = ""; };
31 | F63BFF472908B3D000E18B62 /* TrackableScrollViewTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackableScrollViewTestView.swift; sourceTree = ""; };
32 | F63BFF4929099D7700E18B62 /* SimplePanelTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePanelTestView.swift; sourceTree = ""; };
33 | F63BFF4B2909A33E00E18B62 /* ColorTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorTestView.swift; sourceTree = ""; };
34 | F63BFF53290A391800E18B62 /* SimpleShareSheetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleShareSheetView.swift; sourceTree = ""; };
35 | F6A6B5DD2AC0EAE7001795A8 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; };
36 | /* End PBXFileReference section */
37 |
38 | /* Begin PBXFrameworksBuildPhase section */
39 | F63BFF2A29087E9B00E18B62 /* Frameworks */ = {
40 | isa = PBXFrameworksBuildPhase;
41 | buildActionMask = 2147483647;
42 | files = (
43 | F63BFF4429087F5D00E18B62 /* SimpleCommon in Frameworks */,
44 | );
45 | runOnlyForDeploymentPostprocessing = 0;
46 | };
47 | /* End PBXFrameworksBuildPhase section */
48 |
49 | /* Begin PBXGroup section */
50 | F63BFF2429087E9B00E18B62 = {
51 | isa = PBXGroup;
52 | children = (
53 | F63BFF3E29087EB500E18B62 /* Packages */,
54 | F63BFF2F29087E9B00E18B62 /* simplecommon.example */,
55 | F63BFF2E29087E9B00E18B62 /* Products */,
56 | F63BFF4229087F5D00E18B62 /* Frameworks */,
57 | );
58 | sourceTree = "";
59 | };
60 | F63BFF2E29087E9B00E18B62 /* Products */ = {
61 | isa = PBXGroup;
62 | children = (
63 | F63BFF2D29087E9B00E18B62 /* simplecommon.example.app */,
64 | );
65 | name = Products;
66 | sourceTree = "";
67 | };
68 | F63BFF2F29087E9B00E18B62 /* simplecommon.example */ = {
69 | isa = PBXGroup;
70 | children = (
71 | F63BFF3029087E9B00E18B62 /* simplecommon_exampleApp.swift */,
72 | F63BFF3229087E9B00E18B62 /* ContentView.swift */,
73 | F63BFF4B2909A33E00E18B62 /* ColorTestView.swift */,
74 | F63BFF53290A391800E18B62 /* SimpleShareSheetView.swift */,
75 | F63BFF4929099D7700E18B62 /* SimplePanelTestView.swift */,
76 | F63BFF4029087F1B00E18B62 /* MailTestView.swift */,
77 | F63BFF472908B3D000E18B62 /* TrackableScrollViewTestView.swift */,
78 | F6A6B5DD2AC0EAE7001795A8 /* AppIcon.swift */,
79 | F63BFF3429087E9D00E18B62 /* Assets.xcassets */,
80 | F63BFF3629087E9D00E18B62 /* Preview Content */,
81 | );
82 | path = simplecommon.example;
83 | sourceTree = "";
84 | };
85 | F63BFF3629087E9D00E18B62 /* Preview Content */ = {
86 | isa = PBXGroup;
87 | children = (
88 | F63BFF3729087E9D00E18B62 /* Preview Assets.xcassets */,
89 | );
90 | path = "Preview Content";
91 | sourceTree = "";
92 | };
93 | F63BFF3E29087EB500E18B62 /* Packages */ = {
94 | isa = PBXGroup;
95 | children = (
96 | F63BFF3F29087EB500E18B62 /* SimpleCommon */,
97 | );
98 | name = Packages;
99 | sourceTree = "";
100 | };
101 | F63BFF4229087F5D00E18B62 /* Frameworks */ = {
102 | isa = PBXGroup;
103 | children = (
104 | );
105 | name = Frameworks;
106 | sourceTree = "";
107 | };
108 | /* End PBXGroup section */
109 |
110 | /* Begin PBXNativeTarget section */
111 | F63BFF2C29087E9B00E18B62 /* simplecommon.example */ = {
112 | isa = PBXNativeTarget;
113 | buildConfigurationList = F63BFF3B29087E9D00E18B62 /* Build configuration list for PBXNativeTarget "simplecommon.example" */;
114 | buildPhases = (
115 | F63BFF2929087E9B00E18B62 /* Sources */,
116 | F63BFF2A29087E9B00E18B62 /* Frameworks */,
117 | F63BFF2B29087E9B00E18B62 /* Resources */,
118 | );
119 | buildRules = (
120 | );
121 | dependencies = (
122 | );
123 | name = simplecommon.example;
124 | packageProductDependencies = (
125 | F63BFF4329087F5D00E18B62 /* SimpleCommon */,
126 | );
127 | productName = simplecommon.example;
128 | productReference = F63BFF2D29087E9B00E18B62 /* simplecommon.example.app */;
129 | productType = "com.apple.product-type.application";
130 | };
131 | /* End PBXNativeTarget section */
132 |
133 | /* Begin PBXProject section */
134 | F63BFF2529087E9B00E18B62 /* Project object */ = {
135 | isa = PBXProject;
136 | attributes = {
137 | BuildIndependentTargetsInParallel = 1;
138 | LastSwiftUpdateCheck = 1400;
139 | LastUpgradeCheck = 1400;
140 | TargetAttributes = {
141 | F63BFF2C29087E9B00E18B62 = {
142 | CreatedOnToolsVersion = 14.0.1;
143 | };
144 | };
145 | };
146 | buildConfigurationList = F63BFF2829087E9B00E18B62 /* Build configuration list for PBXProject "simplecommon.example" */;
147 | compatibilityVersion = "Xcode 14.0";
148 | developmentRegion = en;
149 | hasScannedForEncodings = 0;
150 | knownRegions = (
151 | en,
152 | Base,
153 | );
154 | mainGroup = F63BFF2429087E9B00E18B62;
155 | productRefGroup = F63BFF2E29087E9B00E18B62 /* Products */;
156 | projectDirPath = "";
157 | projectRoot = "";
158 | targets = (
159 | F63BFF2C29087E9B00E18B62 /* simplecommon.example */,
160 | );
161 | };
162 | /* End PBXProject section */
163 |
164 | /* Begin PBXResourcesBuildPhase section */
165 | F63BFF2B29087E9B00E18B62 /* Resources */ = {
166 | isa = PBXResourcesBuildPhase;
167 | buildActionMask = 2147483647;
168 | files = (
169 | F63BFF3829087E9D00E18B62 /* Preview Assets.xcassets in Resources */,
170 | F63BFF3529087E9D00E18B62 /* Assets.xcassets in Resources */,
171 | );
172 | runOnlyForDeploymentPostprocessing = 0;
173 | };
174 | /* End PBXResourcesBuildPhase section */
175 |
176 | /* Begin PBXSourcesBuildPhase section */
177 | F63BFF2929087E9B00E18B62 /* Sources */ = {
178 | isa = PBXSourcesBuildPhase;
179 | buildActionMask = 2147483647;
180 | files = (
181 | F63BFF3329087E9B00E18B62 /* ContentView.swift in Sources */,
182 | F63BFF482908B3D000E18B62 /* TrackableScrollViewTestView.swift in Sources */,
183 | F6A6B5DE2AC0EAE7001795A8 /* AppIcon.swift in Sources */,
184 | F63BFF4129087F1B00E18B62 /* MailTestView.swift in Sources */,
185 | F63BFF4A29099D7700E18B62 /* SimplePanelTestView.swift in Sources */,
186 | F63BFF4C2909A33E00E18B62 /* ColorTestView.swift in Sources */,
187 | F63BFF54290A391800E18B62 /* SimpleShareSheetView.swift in Sources */,
188 | F63BFF3129087E9B00E18B62 /* simplecommon_exampleApp.swift in Sources */,
189 | );
190 | runOnlyForDeploymentPostprocessing = 0;
191 | };
192 | /* End PBXSourcesBuildPhase section */
193 |
194 | /* Begin XCBuildConfiguration section */
195 | F63BFF3929087E9D00E18B62 /* Debug */ = {
196 | isa = XCBuildConfiguration;
197 | buildSettings = {
198 | ALWAYS_SEARCH_USER_PATHS = NO;
199 | CLANG_ANALYZER_NONNULL = YES;
200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
202 | CLANG_ENABLE_MODULES = YES;
203 | CLANG_ENABLE_OBJC_ARC = YES;
204 | CLANG_ENABLE_OBJC_WEAK = YES;
205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
206 | CLANG_WARN_BOOL_CONVERSION = YES;
207 | CLANG_WARN_COMMA = YES;
208 | CLANG_WARN_CONSTANT_CONVERSION = YES;
209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
212 | CLANG_WARN_EMPTY_BODY = YES;
213 | CLANG_WARN_ENUM_CONVERSION = YES;
214 | CLANG_WARN_INFINITE_RECURSION = YES;
215 | CLANG_WARN_INT_CONVERSION = YES;
216 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
217 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
220 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
222 | CLANG_WARN_STRICT_PROTOTYPES = YES;
223 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
225 | CLANG_WARN_UNREACHABLE_CODE = YES;
226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
227 | COPY_PHASE_STRIP = NO;
228 | DEBUG_INFORMATION_FORMAT = dwarf;
229 | ENABLE_STRICT_OBJC_MSGSEND = YES;
230 | ENABLE_TESTABILITY = YES;
231 | GCC_C_LANGUAGE_STANDARD = gnu11;
232 | GCC_DYNAMIC_NO_PIC = NO;
233 | GCC_NO_COMMON_BLOCKS = YES;
234 | GCC_OPTIMIZATION_LEVEL = 0;
235 | GCC_PREPROCESSOR_DEFINITIONS = (
236 | "DEBUG=1",
237 | "$(inherited)",
238 | );
239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
241 | GCC_WARN_UNDECLARED_SELECTOR = YES;
242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
243 | GCC_WARN_UNUSED_FUNCTION = YES;
244 | GCC_WARN_UNUSED_VARIABLE = YES;
245 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
246 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
247 | MTL_FAST_MATH = YES;
248 | ONLY_ACTIVE_ARCH = YES;
249 | SDKROOT = iphoneos;
250 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
251 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
252 | };
253 | name = Debug;
254 | };
255 | F63BFF3A29087E9D00E18B62 /* Release */ = {
256 | isa = XCBuildConfiguration;
257 | buildSettings = {
258 | ALWAYS_SEARCH_USER_PATHS = NO;
259 | CLANG_ANALYZER_NONNULL = YES;
260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
262 | CLANG_ENABLE_MODULES = YES;
263 | CLANG_ENABLE_OBJC_ARC = YES;
264 | CLANG_ENABLE_OBJC_WEAK = YES;
265 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
266 | CLANG_WARN_BOOL_CONVERSION = YES;
267 | CLANG_WARN_COMMA = YES;
268 | CLANG_WARN_CONSTANT_CONVERSION = YES;
269 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
270 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
271 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
272 | CLANG_WARN_EMPTY_BODY = YES;
273 | CLANG_WARN_ENUM_CONVERSION = YES;
274 | CLANG_WARN_INFINITE_RECURSION = YES;
275 | CLANG_WARN_INT_CONVERSION = YES;
276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
277 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
278 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
280 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
282 | CLANG_WARN_STRICT_PROTOTYPES = YES;
283 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
285 | CLANG_WARN_UNREACHABLE_CODE = YES;
286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
287 | COPY_PHASE_STRIP = NO;
288 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
289 | ENABLE_NS_ASSERTIONS = NO;
290 | ENABLE_STRICT_OBJC_MSGSEND = YES;
291 | GCC_C_LANGUAGE_STANDARD = gnu11;
292 | GCC_NO_COMMON_BLOCKS = YES;
293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
295 | GCC_WARN_UNDECLARED_SELECTOR = YES;
296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
297 | GCC_WARN_UNUSED_FUNCTION = YES;
298 | GCC_WARN_UNUSED_VARIABLE = YES;
299 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
300 | MTL_ENABLE_DEBUG_INFO = NO;
301 | MTL_FAST_MATH = YES;
302 | SDKROOT = iphoneos;
303 | SWIFT_COMPILATION_MODE = wholemodule;
304 | SWIFT_OPTIMIZATION_LEVEL = "-O";
305 | VALIDATE_PRODUCT = YES;
306 | };
307 | name = Release;
308 | };
309 | F63BFF3C29087E9D00E18B62 /* Debug */ = {
310 | isa = XCBuildConfiguration;
311 | buildSettings = {
312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
313 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
314 | CODE_SIGN_STYLE = Automatic;
315 | CURRENT_PROJECT_VERSION = 1;
316 | DEVELOPMENT_ASSET_PATHS = "\"simplecommon.example/Preview Content\"";
317 | DEVELOPMENT_TEAM = C6L3992RFB;
318 | ENABLE_PREVIEWS = YES;
319 | GENERATE_INFOPLIST_FILE = YES;
320 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
321 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
322 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
323 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
324 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
325 | LD_RUNPATH_SEARCH_PATHS = (
326 | "$(inherited)",
327 | "@executable_path/Frameworks",
328 | );
329 | MARKETING_VERSION = 1.0;
330 | PRODUCT_BUNDLE_IDENTIFIER = "com.twodayslate.simplecommon-example";
331 | PRODUCT_NAME = "$(TARGET_NAME)";
332 | SWIFT_EMIT_LOC_STRINGS = YES;
333 | SWIFT_VERSION = 5.0;
334 | TARGETED_DEVICE_FAMILY = "1,2";
335 | };
336 | name = Debug;
337 | };
338 | F63BFF3D29087E9D00E18B62 /* Release */ = {
339 | isa = XCBuildConfiguration;
340 | buildSettings = {
341 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
342 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
343 | CODE_SIGN_STYLE = Automatic;
344 | CURRENT_PROJECT_VERSION = 1;
345 | DEVELOPMENT_ASSET_PATHS = "\"simplecommon.example/Preview Content\"";
346 | DEVELOPMENT_TEAM = C6L3992RFB;
347 | ENABLE_PREVIEWS = YES;
348 | GENERATE_INFOPLIST_FILE = YES;
349 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
350 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
351 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
352 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
353 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
354 | LD_RUNPATH_SEARCH_PATHS = (
355 | "$(inherited)",
356 | "@executable_path/Frameworks",
357 | );
358 | MARKETING_VERSION = 1.0;
359 | PRODUCT_BUNDLE_IDENTIFIER = "com.twodayslate.simplecommon-example";
360 | PRODUCT_NAME = "$(TARGET_NAME)";
361 | SWIFT_EMIT_LOC_STRINGS = YES;
362 | SWIFT_VERSION = 5.0;
363 | TARGETED_DEVICE_FAMILY = "1,2";
364 | };
365 | name = Release;
366 | };
367 | /* End XCBuildConfiguration section */
368 |
369 | /* Begin XCConfigurationList section */
370 | F63BFF2829087E9B00E18B62 /* Build configuration list for PBXProject "simplecommon.example" */ = {
371 | isa = XCConfigurationList;
372 | buildConfigurations = (
373 | F63BFF3929087E9D00E18B62 /* Debug */,
374 | F63BFF3A29087E9D00E18B62 /* Release */,
375 | );
376 | defaultConfigurationIsVisible = 0;
377 | defaultConfigurationName = Release;
378 | };
379 | F63BFF3B29087E9D00E18B62 /* Build configuration list for PBXNativeTarget "simplecommon.example" */ = {
380 | isa = XCConfigurationList;
381 | buildConfigurations = (
382 | F63BFF3C29087E9D00E18B62 /* Debug */,
383 | F63BFF3D29087E9D00E18B62 /* Release */,
384 | );
385 | defaultConfigurationIsVisible = 0;
386 | defaultConfigurationName = Release;
387 | };
388 | /* End XCConfigurationList section */
389 |
390 | /* Begin XCSwiftPackageProductDependency section */
391 | F63BFF4329087F5D00E18B62 /* SimpleCommon */ = {
392 | isa = XCSwiftPackageProductDependency;
393 | productName = SimpleCommon;
394 | };
395 | /* End XCSwiftPackageProductDependency section */
396 | };
397 | rootObject = F63BFF2529087E9B00E18B62 /* Project object */;
398 | }
399 |
--------------------------------------------------------------------------------