├── 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 | --------------------------------------------------------------------------------