├── Resources ├── Main-screenshot.png ├── Header annotation.png ├── Screenshot-camera.png ├── alert_view_screenshot.png ├── Color-custom-screenshot.png ├── All-permissions-card-new.png └── Deprecated 1.4.0 screenshot.png ├── .gitignore ├── Tests ├── LinuxMain.swift ├── PermissionsSwiftUITests │ ├── XCTestManifests.swift │ ├── __Snapshots__ │ │ └── PermissionsSwiftUITests │ │ │ ├── testAllowButtonAltLabel.1.png │ │ │ ├── testAllowButtonAltLabel.2.png │ │ │ └── testCustomizeHeaderSnapshot.1.png │ └── UITests.swift └── PermissionsSwiftUISmallScreenTests │ └── __Snapshots__ │ └── PermissionsSwiftUISmallScreenTests │ ├── testPermissionCell.1.png │ ├── testPermissionCell.10.png │ ├── testPermissionCell.11.png │ ├── testPermissionCell.12.png │ ├── testPermissionCell.13.png │ ├── testPermissionCell.14.png │ ├── testPermissionCell.15.png │ ├── testPermissionCell.16.png │ ├── testPermissionCell.17.png │ ├── testPermissionCell.18.png │ ├── testPermissionCell.19.png │ ├── testPermissionCell.2.png │ ├── testPermissionCell.20.png │ ├── testPermissionCell.21.png │ ├── testPermissionCell.22.png │ ├── testPermissionCell.23.png │ ├── testPermissionCell.24.png │ ├── testPermissionCell.25.png │ ├── testPermissionCell.26.png │ ├── testPermissionCell.27.png │ ├── testPermissionCell.28.png │ ├── testPermissionCell.29.png │ ├── testPermissionCell.3.png │ ├── testPermissionCell.30.png │ ├── testPermissionCell.31.png │ ├── testPermissionCell.32.png │ ├── testPermissionCell.33.png │ ├── testPermissionCell.34.png │ ├── testPermissionCell.35.png │ ├── testPermissionCell.36.png │ ├── testPermissionCell.37.png │ ├── testPermissionCell.38.png │ ├── testPermissionCell.39.png │ ├── testPermissionCell.4.png │ ├── testPermissionCell.5.png │ ├── testPermissionCell.6.png │ ├── testPermissionCell.7.png │ ├── testPermissionCell.8.png │ ├── testPermissionCell.9.png │ ├── testAlertViewInitializers.1.png │ ├── testAlertViewInitializers.2.png │ ├── testAlertViewInitializers.3.png │ ├── testAlertViewInitializers.4.png │ ├── testAlertViewInitializers.5.png │ ├── testAlertViewInitializers.6.png │ ├── testAlertViewInitializers.7.png │ ├── testModalViewSnapshot14_0.1.png │ ├── testAlertViewSinglePermission.1.png │ ├── testAlertViewThreePermissions.1.png │ ├── testAlertViewTwoPermissions.1.png │ └── testCustomizeHeaderSnapshot.1.png ├── Sources ├── CorePermissionsSwiftUI │ ├── Utility │ │ ├── Custom │ │ │ ├── NilAssignOperator.swift │ │ │ └── Haptic.swift │ │ └── Extensions │ │ │ ├── String.swift │ │ │ └── ColorsAvailability.swift │ ├── Resources │ │ ├── da.lproj │ │ │ └── Localizable.strings │ │ ├── fi.lproj │ │ │ └── Localizable.strings │ │ ├── zh-Hans.lproj │ │ │ └── Localizable.strings │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ ├── it.lproj │ │ │ └── Localizable.strings │ │ ├── tr.lproj │ │ │ └── Localizable.strings │ │ ├── de.lproj │ │ │ └── Localizable.strings │ │ └── fr.lproj │ │ │ └── Localizable.strings │ ├── Modifiers │ │ ├── Internal │ │ │ ├── AnyViewModifier.swift │ │ │ ├── EnvironmentObject.swift │ │ │ └── ViewModifiers.swift │ │ └── Public │ │ │ ├── MainTextModifiers.swift │ │ │ └── PermissionCustomModifiers.swift │ ├── Model │ │ ├── Mocks │ │ │ ├── MockPhotoManager.swift │ │ │ ├── MockLocationManager.swift │ │ │ ├── MockNotificationManager.swift │ │ │ └── MockHealthManager.swift │ │ ├── PermissionType │ │ │ ├── PermissionTypeGetSet.swift │ │ │ └── PermissionType.swift │ │ ├── Structs │ │ │ ├── JMResult.swift │ │ │ ├── FilterPermissions.swift │ │ │ └── JMPermission.swift │ │ └── PermissionManagers │ │ │ ├── AuthorizationStatus.swift │ │ │ └── PermissionManager.swift │ ├── SwiftUI │ │ ├── Shared │ │ │ ├── Blur.swift │ │ │ ├── PermissionSection.swift │ │ │ ├── AllowButtonSection.swift │ │ │ ├── ExitButtonSection.swift │ │ │ ├── HeaderView.swift │ │ │ └── PermissionSectionCell.swift │ │ ├── ViewProtocols │ │ │ └── CustomizableView.swift │ │ ├── Dialog-style │ │ │ ├── DialogView.swift │ │ │ └── DialogViewWrapper.swift │ │ └── Modal-style │ │ │ ├── ModalView.swift │ │ │ └── ModalViewWrapper.swift │ └── Store │ │ ├── StoreProtocols │ │ └── ComponentsStore.swift │ │ ├── ConfigStore │ │ ├── ButtonColor.swift │ │ ├── ConfigStore.swift │ │ └── AllButtonColors.swift │ │ ├── PermissionStore │ │ ├── PermissionStore.swift │ │ └── BackwardCompatibilityExtensions.swift │ │ ├── PermissionSchemaStore.swift │ │ └── ComponentsStore │ │ └── PermissionComponentsStore.swift ├── PermissionsSwiftUIMicrophone │ └── JMMicPermissionManager.swift ├── PermissionsSwiftUICamera │ └── JMCameraPermissionManager.swift ├── PermissionsSwiftUISiri │ └── JMSiriPermissionManager.swift ├── PermissionsSwiftUIMusic │ └── JMMusicPermissionManager.swift ├── PermissionsSwiftUISpeech │ └── JMSpeechPermissionManager.swift ├── PermissionsSwiftUIContacts │ └── JMContactsPermissionManager.swift ├── PermissionsSwiftUIReminder │ └── JMRemindersPermissionManager.swift ├── PermissionsSwiftUITracking │ └── JMTrackingPermissionManager.swift ├── PermissionsSwiftUIBluetooth │ └── JMBluetoothPermissionManager.swift ├── PermissionsSwiftUIMotion │ └── JMMotionPermissionManager.swift ├── PermissionsSwiftUIEvent │ └── EventPermissionManager.swift ├── PermissionsSwiftUI │ └── API.swift ├── PermissionsSwiftUINotification │ └── JMNotificationPermissionManager.swift ├── PermissionsSwiftUIHealth │ ├── HKAccess.swift │ └── JMHealthPermissionManager.swift ├── PermissionsSwiftUICalendar │ └── JMCalendarPermissionManager.swift ├── PermissionsSwiftUILocation │ └── JMLocationPermissionManager.swift ├── PermissionsSwiftUILocationAlways │ └── JMLocationAlwaysPermissionManager.swift ├── PermissionsSwiftUIBiometrics │ └── JMBiometricsPermissionManager.swift └── PermissionsSwiftUIPhoto │ └── JMPhotoPermissionManager.swift ├── .swiftformat ├── .github ├── documentation_coverage.sh ├── workflows │ ├── jazzy.yml │ ├── stale.yml │ └── swift.yml └── ISSUE_TEMPLATE │ ├── action-item.md │ ├── feature_request.md │ └── bug_report.md ├── PermissionsSwiftUI.podspec ├── LICENSE ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── docs └── New_Permission_Guide.md ├── Package.swift └── README.md /Resources/Main-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/Main-screenshot.png -------------------------------------------------------------------------------- /Resources/Header annotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/Header annotation.png -------------------------------------------------------------------------------- /Resources/Screenshot-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/Screenshot-camera.png -------------------------------------------------------------------------------- /Resources/alert_view_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/alert_view_screenshot.png -------------------------------------------------------------------------------- /Resources/Color-custom-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/Color-custom-screenshot.png -------------------------------------------------------------------------------- /Resources/All-permissions-card-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/All-permissions-card-new.png -------------------------------------------------------------------------------- /Resources/Deprecated 1.4.0 screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Resources/Deprecated 1.4.0 screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swiftpm/ 3 | .swiftpm/* 4 | /*.xcodeproj 5 | /.build 6 | /build 7 | /Packages 8 | xcuserdata/ 9 | docs/ 10 | Package.resolved -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PermissionsSwiftUITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PermissionsSwiftUITests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Utility/Custom/NilAssignOperator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 4/1/21. 6 | // 7 | 8 | import Foundation 9 | 10 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/da.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "TILLADE"; 3 | "button_allowed" = "TILLADT"; 4 | "button_denied" = "NÆGTET"; 5 | "button_next" = "NÆSTE"; 6 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/fi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "SALLIA"; 3 | "button_allowed" = "SALLITTUA"; 4 | "button_denied" = "KIELLETTY"; 5 | "button_next" = "SEURAAVA"; 6 | 7 | -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUITests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | [ 6 | testCase(PermissionsSwiftUITests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUITests/__Snapshots__/PermissionsSwiftUITests/testAllowButtonAltLabel.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUITests/__Snapshots__/PermissionsSwiftUITests/testAllowButtonAltLabel.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUITests/__Snapshots__/PermissionsSwiftUITests/testAllowButtonAltLabel.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUITests/__Snapshots__/PermissionsSwiftUITests/testAllowButtonAltLabel.2.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUITests/__Snapshots__/PermissionsSwiftUITests/testCustomizeHeaderSnapshot.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUITests/__Snapshots__/PermissionsSwiftUITests/testCustomizeHeaderSnapshot.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.10.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.11.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.12.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.13.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.14.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.15.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.16.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.17.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.18.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.19.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.2.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.20.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.21.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.22.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.23.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.24.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.25.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.26.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.27.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.28.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.29.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.3.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.30.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.31.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.32.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.33.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.34.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.35.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.36.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.37.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.38.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.39.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.4.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.5.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.6.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.7.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.8.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testPermissionCell.9.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.2.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.3.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.4.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.5.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.6.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewInitializers.7.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testModalViewSnapshot14_0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testModalViewSnapshot14_0.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewSinglePermission.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewSinglePermission.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewThreePermissions.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewThreePermissions.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewTwoPermissions.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testAlertViewTwoPermissions.1.png -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testCustomizeHeaderSnapshot.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevonmao/PermissionsSwiftUI/HEAD/Tests/PermissionsSwiftUISmallScreenTests/__Snapshots__/PermissionsSwiftUISmallScreenTests/testCustomizeHeaderSnapshot.1.png -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --binarygrouping none 2 | --decimalgrouping none 3 | --elseposition next-line 4 | --hexgrouping none 5 | --ifdef no-indent 6 | --octalgrouping none 7 | --self insert 8 | --semicolons never 9 | --stripunusedargs closure-only 10 | --trimwhitespace nonblank-lines 11 | --wraparguments before-first 12 | --wrapcollections before-first 13 | 14 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Modifiers/Internal/AnyViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/19/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | extension View { 12 | @usableFromInline func typeErased() -> AnyView { 13 | AnyView(self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Mocks/MockPhotoManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPhotoManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import Foundation 9 | #if PERMISSIONSWIFTUI_PHOTO 10 | import Photos 11 | 12 | final class MockPhotoManager: PHPhotoLibrary{ 13 | static override func requestAuthorization(_ handler: @escaping (PHAuthorizationStatus) -> Void) { 14 | handler(PHAuthorizationStatus.authorized) 15 | } 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/PermissionType/PermissionTypeGetSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionModelGet.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @available(iOS 13.0, tvOS 13.0, *) 12 | extension PermissionType { 13 | var rawValue: String { 14 | guard let label = Mirror(reflecting: self).children.first?.label else { 15 | return .init(describing: self) 16 | } 17 | return label 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Utility/Extensions/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 12/27/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | // https://stackoverflow.com/questions/25081757/whats-nslocalizedstring-equivalent-in-swift 12 | /// Swifty wrapper for accessing NSLocalizedString with only key parameter 13 | var localized: String { 14 | return NSLocalizedString(self, tableName: nil, bundle: .module, value: "", comment: "") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/PermissionsSwiftUITests/UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITests.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 6/15/21. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | final class UITests: XCTestCase { 12 | var app: XCUIApplication! 13 | 14 | override func setUp() { 15 | continueAfterFailure = false 16 | app = XCUIApplication() 17 | app.launchArguments = ["testing"] 18 | app.launch() 19 | } 20 | 21 | func testTitle() { 22 | //XCTAssertTrue(app.staticTexts["Hello World!"].exists) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Modifiers/Internal/EnvironmentObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 4/6/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | extension View { 12 | @usableFromInline func withEnvironmentObjects(store: PermissionStore, permissionStyle: PermissionViewStyle) -> some View { 13 | self 14 | .environmentObject(store) 15 | .environmentObject(PermissionSchemaStore(store: store, 16 | permissionViewStyle: permissionStyle)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "允许"; 3 | "button_allowed" = "已允许"; 4 | "button_denied" = "未允许"; 5 | "button_next" = "继续"; 6 | 7 | /* MARK: UI labels */ 8 | "permission_header" = "需要权限"; 9 | "permission_primary_label" = "请求允许权限来使用本app的相关功能。请见每个权限的描述。"; 10 | "permission_secondary_label" = "所有功能的正常使用需要允许权限。如果未允许,需要在设置中启用权限。"; 11 | 12 | /* MARK: Permissions (name & description) */ 13 | "camera_title" = "相机"; 14 | "camera_description" = "允许使用相机"; 15 | 16 | "health_title" = "健康"; 17 | "health_description" = "允许访问您的健康信息"; 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/documentation_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | percentage=$(jazzy --module CorePermissionsSwiftUI --swift-build-tool xcodebuild --build-tool-arguments -scheme,PermissionsSwiftUI,-sdk,iphoneos,-destination,id=A06F71FC-0B53-4741-AD85-D237B6FB06A0,-verbose --author 'Jevon Mao' --author_url https://jingwen-mao.mit-license.org/ | tail -4 | head -n 1 | cut -d " " -f1 4 | ) 5 | 6 | #if [ $? -eq 0 ] 7 | #then 8 | # echo "/var/folders/24/" 9 | if test "${percentage%?}" == "100" 10 | then 11 | echo "Documentation coverage 100%, test passed." 12 | else 13 | echo "Documentation coverage is $percentage%, which is below 100%, test failed." 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /.github/workflows/jazzy.yml: -------------------------------------------------------------------------------- 1 | name: PublishDocumentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy_docs: 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Publish Jazzy Docs 13 | uses: steven0351/publish-jazzy-docs@v1 14 | with: 15 | personal_access_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 16 | args: "--module PermissionsSwiftUI --swift-build-tool xcodebuild --build-tool-arguments -scheme,PermissionsSwiftUI,-sdk,iphoneos,-destination,id=A06F71FC-0B53-4741-AD85-D237B6FB06A0,-verbose --author 'Jevon Mao' --author_url https://jingwen-mao.mit-license.org/" 17 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Utility/Custom/Haptic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Haptic.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/4/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | @available(tvOS, unavailable) 12 | class HapticsManager { 13 | var notificationFeedbackGenerator: UINotificationFeedbackGenerator? 14 | init() { 15 | notificationFeedbackGenerator = UINotificationFeedbackGenerator() 16 | notificationFeedbackGenerator?.prepare() 17 | } 18 | func notificationImpact(_ type: UINotificationFeedbackGenerator.FeedbackType){ 19 | notificationFeedbackGenerator?.notificationOccurred(type) 20 | notificationFeedbackGenerator = nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/action-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Action Item 3 | about: Keep track of implementation progress 4 | title: '' 5 | labels: help wanted, good first issue, work in progres 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Overview 11 | An overview description of this task, along with a reference to the actual bug or feature issue like: tracking feature `#72`. 12 | 13 | ### Action items 14 | 15 | - [ ] Step 1... 16 | - [ ] Step 2... 17 | - [ ] Step 3... 18 | 19 | 20 | ### Visual References 21 | Put all the instructional images, visuals, and prototypes here for reference. 22 | 23 | ### Special Instructions 24 | Add special instructions, warnings, previous issues, and other related stuff here, if applicable. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Shared/Blur.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blur.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | let screenSize = UIScreen.main.bounds 11 | 12 | @available(iOS 13.0, tvOS 13.0, *) 13 | struct Blur: UIViewRepresentable { 14 | #if !os(tvOS) 15 | var style: UIBlurEffect.Style = .systemMaterial 16 | #else 17 | var style: UIBlurEffect.Style = .regular 18 | #endif 19 | func makeUIView(context: Context) -> UIVisualEffectView { 20 | return UIVisualEffectView(effect: UIBlurEffect(style: style)) 21 | } 22 | func updateUIView(_ uiView: UIVisualEffectView, context: Context) { 23 | uiView.effect = UIBlurEffect(style: style) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Structs/JMResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMResult.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/6/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A structure that encapsulates a permission request result 12 | 13 | When no error occured during the permission request process, the `error`property will be `nil`. The authorization status will reflect the permission's authorization status. 14 | */ 15 | @available(iOS 13.0, tvOS 13.0, *) 16 | public struct JMResult { 17 | ///The type of permission for the result 18 | public let permissionType: PermissionType 19 | ///The authorization status of the permission 20 | public let authorizationStatus: AuthorizationStatus 21 | ///An error object containing why the permission request failed, or nil if the operation was successful 22 | public let error: Error? 23 | } 24 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "ALLOW"; 3 | "button_allowed" = "ALLOWED"; 4 | "button_denied" = "DENIED"; 5 | "button_next" = "NEXT"; 6 | 7 | /* MARK: UI labels */ 8 | "permission_header" = "Need Permissions"; 9 | "permission_primary_label" = "In order for you use certain features of this app, you need to give permissions. See description for each permission"; 10 | "permission_secondary_label" = "Permission are necessary for all the features and functions to work properly. If not allowed, you have to enable permissions in settings"; 11 | 12 | /* MARK: Permissions (name & description) */ 13 | "camera_title" = "Camera"; 14 | "camera_description" = "Allow to use your camera"; 15 | 16 | "health_title" = "Health"; 17 | "health_description" = "Allow to access your health information"; 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/PermissionManagers/AuthorizationStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationStatus.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/18/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | The authorization status for any iOS system permission 12 | */ 13 | public enum AuthorizationStatus: String, Hashable, Equatable { 14 | ///The explicitly allowed or `authorized` permission state 15 | case authorized 16 | ///The explicitly denied permission state 17 | case denied 18 | ///The state in which the user has granted limited access permission (ex. photos) 19 | case limited 20 | ///The temporary allowed state that limits the app's access (ex. allow once) 21 | case temporary 22 | ///The `notDetermined` permission state, and the only state where it is possible to ask permission 23 | case notDetermined 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "ALLOW"; 3 | "button_allowed" = "ALLOWED"; 4 | "button_denied" = "DENIED"; 5 | "button_next" = "NEXT"; 6 | 7 | /* MARK: UI labels */ 8 | "permission_header" = "Need Permissions"; 9 | 10 | "permission_primary_label" = "In order for you use certain features of this app, you need to give permissions. See description for each permission"; 11 | 12 | "permission_secondary_label" = "Permission are necessary for all the features and functions to work properly. If not allowed, you have to enable permissions in settings"; 13 | 14 | /* MARK: Permissions (name & description) */ 15 | "camera_title" = "Camera"; 16 | "camera_description" = "Allow to use your camera"; 17 | 18 | "health_title" = "Health"; 19 | "health_description" = "Allow to access your health information"; 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Shared/PermissionSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionSection.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | struct PermissionSection: View { 12 | @Environment(\.colorScheme) var colorScheme 13 | @Binding var showing:Bool 14 | @EnvironmentObject var store: PermissionStore 15 | 16 | var body: some View { 17 | VStack { 18 | ForEach(Array(zip(store.permissions.indices, store.permissions)), id: \.0) {index, permission in 19 | PermissionSectionCell(permissionManager: permission, showing: $showing) 20 | if store.permissions.count > 1 && index != store.permissions.count - 1{ 21 | Divider() 22 | } 23 | } 24 | } 25 | 26 | 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/tr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "İzin Ver"; 3 | "button_allowed" = "İzin Verildi"; 4 | "button_denied" = "Reddedildi"; 5 | "button_next" = "SIRADAKİ"; 6 | 7 | /* MARK: UI labels */ 8 | "permission_header" = "İzinlerinize ihtiyacımız var"; 9 | "permission_primary_label" = "Bu uygulamanın belirli özelliklerini kullanabilmeniz için izin vermeniz gerekir. Her izin için izne ait açıklamaya bakın"; 10 | "permission_secondary_label" = "Tüm özelliklerin ve işlevlerin düzgün çalışması için izin gereklidir. İzin vermezseniz efektif kullanım için ayarlardan izinleri tekrardan etkinleştirmeniz gerekir."; 11 | 12 | /* MARK: Permissions (name & description) */ 13 | "camera_title" = "Kamera"; 14 | "camera_description" = "Kameranızın kullanılmasına izin verin"; 15 | 16 | "health_title" = "Sağlık"; 17 | "health_description" = "Sağlık bilgilerinizin kullanılmasına izin verin"; 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # name: Mark stale issues and pull requests 2 | 3 | # on: 4 | # schedule: 5 | # - cron: "30 1 * * *" 6 | 7 | # jobs: 8 | # stale: 9 | 10 | # runs-on: ubuntu-latest 11 | 12 | # steps: 13 | # - uses: actions/stale@v3 14 | # with: 15 | # repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | # stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 17 | # stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 18 | # stale-issue-label: 'stale' 19 | # stale-pr-label: 'stale' 20 | # days-before-stale: 30 21 | # days-before-close: 3 22 | # exempt-issue-labels: "enhancement,confirmed bug,work in progress" 23 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "ERLAUBEN"; 3 | "button_allowed" = "ERLAUBT"; 4 | "button_denied" = "ABGELEHNT"; 5 | "button_next" = "WEITER"; 6 | 7 | /* MARK: UI labels */ 8 | "permission_header" = "Berechtigungen benötigt"; 9 | 10 | "permission_primary_label" = "Damit Sie bestimmte Funktionen dieser App benutzen können, müssen Sie Berechtigungen zulassen. Siehe Beschreiben der Berechtigung"; 11 | 12 | "permission_secondary_label" = "Um alle Funktionen ordnungsgemäß benutzen zu können, müssen bestimmte Berechtigungen erteilt werden. Wenn Sie die Berechtigungen nicht erlauben, müssen Sie diese in den Einstellungen erlauben."; 13 | 14 | /* MARK: Permissions (name & description) */ 15 | "camera_title" = "Kamera"; 16 | "camera_description" = "Erlaube die Verwendung der Kamera"; 17 | 18 | "health_title" = "Health"; 19 | "health_description" = "Erlauben Sie den Zugriff auf Ihre Gesundheitsinformationen"; 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Resources/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* MARK: Allow Button */ 2 | "button_allow" = "AUTORISER"; 3 | "button_allowed" = "AUTORISÉ"; 4 | "button_denied" = "REFUSÉ"; 5 | "button_next" = "SUIVANT"; 6 | 7 | /* MARK: UI labels */ 8 | "permission_header" = "Autorisations Nécessaires"; 9 | 10 | "permission_primary_label" = "Pour pouvoir utiliser certaines fonctionnalités de cette application, vous devez donner des autorisations. Voir la description de chaque autorisation"; 11 | 12 | 13 | "permission_secondary_label" = "Une autorisation est nécessaire pour que toutes les fonctionnalités et fonctions fonctionnent correctement. Si ce n'est pas autorisé, vous devez activer les autorisations dans les paramètres"; 14 | 15 | 16 | /* MARK: Permissions (name & description) */ 17 | "camera_title" = "Caméra"; 18 | "camera_description" = "Autoriser l'utilisation de la caméra"; 19 | 20 | "health_title" = "Santé"; 21 | "health_description" = "Autoriser l'accés aux informations de santé"; 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] - " 5 | labels: possible bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/StoreProtocols/ComponentsStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/26/21. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | protocol ComponentsStore { 12 | var cameraPermission: JMPermission {get set} 13 | var locationPermission: JMPermission {get set} 14 | var locationAlwaysPermission: JMPermission {get set} 15 | var photoPermission: JMPermission {get set} 16 | var microphonePermisson: JMPermission {get set} 17 | var notificationPermission: JMPermission {get set} 18 | var calendarPermisson: JMPermission {get set} 19 | var bluetoothPermission: JMPermission {get set} 20 | var trackingPermission: JMPermission {get set} 21 | var contactsPermission: JMPermission {get set} 22 | var motionPermission: JMPermission {get set} 23 | var remindersPermission: JMPermission {get set} 24 | var speechPermission: JMPermission {get set} 25 | var healthPermission: JMPermission {get set} 26 | var musicPermission: JMPermission {get set} 27 | } 28 | 29 | -------------------------------------------------------------------------------- /PermissionsSwiftUI.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'PermissionsSwiftUI' 3 | s.version = '1.4.3' 4 | s.summary = 'A SwiftUI package to beautifully display and handle permissions.' 5 | 6 | s.description = <<-DESC 7 | PermissionsSwiftUI can display either a modal or alert popover to show an interactive permissions request view with button. All the text and colors are highly configuration and customizable. 8 | DESC 9 | 10 | s.homepage = 'https://github.com/jevonmao/PermissionsSwiftUI' 11 | s.license = { :type => 'MIT', :file => 'LICENSE' } 12 | s.author = { '' => '' } 13 | s.source = { :git => 'https://github.com/jevonmao/PermissionsSwiftUI.git', :tag => '1.4.0' } 14 | s.swift_version = '5.0' 15 | s.ios.deployment_target = '11.0' 16 | s.source_files = 'Sources/**/*.swift', 'Sources/**/**/*.swift', 'Sources/**/**/**/*.swift' 17 | 18 | s.dependency 'Introspect' 19 | s.dependency 'PermissionsSwiftUI/CorePermissionsSwiftUI' 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jingwen Mao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/ConfigStore/ButtonColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/18/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | `ButtonColor` represents the color configuration for the allow button in a single state 12 | 13 | Declared within parent struct `AllButtonColors` and should only be used within a `AllButtonColors` struct instance. 14 | To customize 15 | */ 16 | @available(iOS 13.0, tvOS 13.0, *) 17 | public struct ButtonColor: Equatable { 18 | // MARK: Creating New Button Color 19 | /** 20 | - parameters: 21 | - foregroundColor: The color of type `Color` for the foreground text 22 | - backgroundColor: The color of type `Color` for the background 23 | */ 24 | public init(foregroundColor: Color, backgroundColor: Color){ 25 | self.foregroundColor = foregroundColor 26 | self.backgroundColor = backgroundColor 27 | } 28 | //MARK: Properties 29 | ///The color of type `Color` for the foreground text 30 | public var foregroundColor: Color 31 | ///The color of type `Color` for the foreground text 32 | public var backgroundColor: Color 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Shared/AllowButtonSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllowButtonSection.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | struct AllowButtonSection: View { 12 | var action: () -> Void 13 | var useAltText: Bool 14 | 15 | @Binding var allowButtonStatus: AllowButtonStatus 16 | 17 | var buttonText: String { 18 | if allowButtonStatus == .allowed { 19 | return "button_allowed" 20 | } 21 | if allowButtonStatus == .idle { 22 | if useAltText { 23 | return "button_next" 24 | } 25 | return "button_allow" 26 | } 27 | 28 | return "button_denied" 29 | } 30 | 31 | var body: some View { 32 | Button(action: action, label: { 33 | Text(LocalizedStringKey(buttonText), bundle: .module) 34 | .fontWeight(.bold) 35 | .buttonStatusColor(for: allowButtonStatus) 36 | }) 37 | .layoutPriority(-1) 38 | .accessibility(identifier: "Allow button") 39 | .animation(.easeInOut, value: buttonText) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/ViewProtocols/CustomizableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/19/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Creates any style of PermissionsSwiftUI view with custom configurations 12 | 13 | The `JMModal` and `JMAlert` modifier are higher-level containers for `AlertMainView` and `ModalMainView`. Both of those conform to the `CustomizableView` protocol, which allows it to be configured with PermissionSwiftUI's customization modifiers. 14 | */ 15 | @available(iOS 13.0, tvOS 13.0, *) 16 | public protocol CustomizableView: View { 17 | //MARK: Environment data storage 18 | ///A global data storage object that is implemented by views 19 | var store: PermissionStore {get} 20 | ///A schema storage object that is implemented by views 21 | var schemaStore: PermissionSchemaStore {get} 22 | ///Concrete type should be constraint to of type View 23 | associatedtype ViewType 24 | ///Binding variable to control the presentable of view 25 | var showing: Binding {get set} 26 | ///The layer of passed in view below PermisisonsSwiftUI's view 27 | var bodyView: ViewType {get set} 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIMicrophone/JMMicPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMMicPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | #if os(iOS) 8 | import AVFoundation 9 | import Foundation 10 | import CorePermissionsSwiftUI 11 | 12 | @available(iOS 13.0, tvOS 13.0, *) 13 | public extension PermissionManager { 14 | ///Permission allows developers to interact with the device microphone 15 | static let microphone = JMMicrophonePermissionManager() 16 | } 17 | 18 | @available(iOS 13.0, tvOS 13.0, *) 19 | public final class JMMicrophonePermissionManager: PermissionManager { 20 | public override var permissionType: PermissionType { 21 | .microphone 22 | } 23 | public override var authorizationStatus: AuthorizationStatus { 24 | switch AVCaptureDevice.authorizationStatus(for: .audio){ 25 | case .authorized: 26 | return .authorized 27 | case .notDetermined: 28 | return .notDetermined 29 | default: 30 | return .denied 31 | } 32 | } 33 | 34 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 35 | AVAudioSession.sharedInstance().requestRecordPermission { 36 | granted in 37 | DispatchQueue.main.async { 38 | completion(granted, nil) 39 | } 40 | } 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUICamera/JMCameraPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMCameraPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | #if os(iOS) 8 | import AVFoundation 9 | import Foundation 10 | import CorePermissionsSwiftUI 11 | 12 | @available(iOS 13.0, tvOS 13.0, *) 13 | public extension PermissionManager { 14 | ///Permission that allows developers to interact with on-device camera 15 | static let camera = JMCameraPermissionManager() 16 | } 17 | 18 | @available(iOS 13.0, tvOS 13.0, *) 19 | public final class JMCameraPermissionManager: PermissionManager { 20 | 21 | 22 | public override var permissionType: PermissionType { 23 | .camera 24 | } 25 | 26 | public override var authorizationStatus: AuthorizationStatus { 27 | switch AVCaptureDevice.authorizationStatus(for: .video){ 28 | case .authorized: 29 | return .authorized 30 | case .notDetermined: 31 | return .notDetermined 32 | default: 33 | return .denied 34 | } 35 | } 36 | 37 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 38 | AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { 39 | authorized in 40 | DispatchQueue.main.async { 41 | completion(authorized, nil) 42 | } 43 | }) 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUISiri/JMSiriPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMSiriPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 6/14/21. 6 | // 7 | 8 | import Foundation 9 | import CorePermissionsSwiftUI 10 | import Intents 11 | 12 | @available(iOS 13.0, tvOS 13.0, *) 13 | public extension PermissionManager { 14 | ///Permission that allows Siri and Maps to communicate with your app 15 | static let siri = JMSiriPermissionManager() 16 | } 17 | 18 | @available(iOS 13.0, tvOS 13.0, *) 19 | public final class JMSiriPermissionManager: PermissionManager { 20 | 21 | 22 | public override var permissionType: PermissionType { 23 | .siri 24 | } 25 | 26 | public override var authorizationStatus: AuthorizationStatus { 27 | switch INPreferences.siriAuthorizationStatus() { 28 | case .authorized: 29 | return .authorized 30 | case .notDetermined: 31 | return .notDetermined 32 | default: 33 | return .denied 34 | } 35 | } 36 | 37 | public override func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 38 | INPreferences.requestSiriAuthorization {authorizationStatus in 39 | if authorizationStatus == .authorized { 40 | completion(true, nil) 41 | } 42 | else { 43 | completion(false, nil) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIMusic/JMMusicPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMMusicPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/22/21. 6 | // 7 | 8 | import Foundation 9 | import MediaPlayer 10 | import CorePermissionsSwiftUI 11 | 12 | #if !os(tvOS) 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | ///Permission that allows app to control audio playback of the device 16 | static let music = JMMusicPermissionManager() 17 | } 18 | 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public final class JMMusicPermissionManager: PermissionManager { 21 | 22 | public override var authorizationStatus: AuthorizationStatus { 23 | switch MPMediaLibrary.authorizationStatus(){ 24 | case .authorized: 25 | return .authorized 26 | case .notDetermined: 27 | return .notDetermined 28 | default: 29 | return .denied 30 | } 31 | } 32 | public override var permissionType: PermissionType { 33 | .music 34 | } 35 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 36 | MPMediaLibrary.requestAuthorization {authStatus in 37 | switch authStatus{ 38 | case .authorized: 39 | completion(true, nil) 40 | case .notDetermined: 41 | break 42 | default: 43 | completion(false, nil) 44 | } 45 | } 46 | } 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Mocks/MockLocationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockLocationManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | #if PERMISSIONSWIFTUI_LOCATION 10 | import MapKit 11 | 12 | protocol LocationManager { 13 | var delegate: CLLocationManagerDelegate? {get set} 14 | func authorizationStatus() -> CLAuthorizationStatus 15 | func requestWhenInUseAuthorization() 16 | #if !os(tvOS) 17 | func requestAlwaysAuthorization() 18 | #endif 19 | } 20 | 21 | 22 | extension CLLocationManager: LocationManager{ 23 | func authorizationStatus() -> CLAuthorizationStatus { 24 | CLLocationManager.authorizationStatus() 25 | } 26 | 27 | 28 | } 29 | struct MockCLLocationManager:LocationManager{ 30 | weak var delegate: CLLocationManagerDelegate? 31 | 32 | private static var status:CLAuthorizationStatus = .notDetermined 33 | var whenInUseRequestOverride: CLAuthorizationStatus = .authorizedWhenInUse 34 | var alwaysUseRequestOverride: CLAuthorizationStatus = .authorizedAlways 35 | 36 | func authorizationStatus() -> CLAuthorizationStatus { 37 | MockCLLocationManager.status 38 | } 39 | 40 | func requestWhenInUseAuthorization() { 41 | MockCLLocationManager.status = whenInUseRequestOverride 42 | } 43 | 44 | func requestAlwaysAuthorization() { 45 | MockCLLocationManager.status = alwaysUseRequestOverride 46 | 47 | } 48 | 49 | 50 | } 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUISpeech/JMSpeechPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMSpeechPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | #if !os(tvOS) 10 | import Speech 11 | import CorePermissionsSwiftUI 12 | 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | ///Permission that allows app to use speech recognition 16 | static let speech = JMSpeechPermissionManager() 17 | } 18 | 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public final class JMSpeechPermissionManager: PermissionManager { 21 | 22 | 23 | public override var permissionType: PermissionType { 24 | .speech 25 | } 26 | 27 | public override var authorizationStatus: AuthorizationStatus { 28 | switch SFSpeechRecognizer.authorizationStatus(){ 29 | case .authorized: 30 | return .authorized 31 | case .notDetermined: 32 | return .notDetermined 33 | default: 34 | return .denied 35 | } 36 | } 37 | 38 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 39 | SFSpeechRecognizer.requestAuthorization {authStatus in 40 | switch authStatus{ 41 | case .authorized: 42 | completion(true, nil) 43 | case .notDetermined: 44 | break 45 | default: 46 | completion(false, nil) 47 | } 48 | } 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/PermissionStore/PermissionStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionModelSet.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | //MARK: - Storage 12 | /** 13 | The data storage class that contains reference to all the custom configurations 14 | 15 | - SeeAlso: PermissionSchemaStore 16 | */ 17 | @available(iOS 13.0, tvOS 13.0, *) 18 | public class PermissionStore: ObservableObject { 19 | 20 | //MARK: Creating a new store 21 | /** 22 | Initalizes and returns a new instance of `PermissionStore` 23 | 24 | - Returns: A configuration and customizable data store 25 | 26 | The `PermissionStore` initliazer accepts no parameters, instead, set properties after intialization: 27 | ``` 28 | let store = PermissionStore() 29 | store.mainTexts.headerText = "PermissionsSwiftUI is the best library" 30 | */ 31 | public init(){} 32 | 33 | ///An array of permissions that configures the permissions to request 34 | public var permissions: [PermissionManager] = [] 35 | 36 | //MARK: Configuration store 37 | ///Custom configurations that alters PermissionsSwiftUI view's behaviors 38 | public var configStore = ConfigStore() 39 | 40 | //MARK: Permission components store 41 | /** 42 | Customizable displayed component for each PermissionType (types of permission) 43 | */ 44 | public var permissionComponentsStore = PermissionComponentsStore() 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Structs/FilterPermissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterPermissions.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/19/21. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | struct FilterPermissions { 12 | // Based on struct boolean property, dependent on memory 13 | static func filterForUnauthorized(with permissions: [PermissionManager], 14 | store: PermissionSchemaStore) -> [PermissionManager] { 15 | let filteredPermissions = permissions.filter { 16 | store.permissionComponentsStore.getPermissionComponent(for: $0.permissionType).authorized == false 17 | } 18 | return filteredPermissions 19 | } 20 | 21 | // static func filterForInteracted(for permissions: [PermissionType]) -> [PermissionType] { 22 | // var filteredPermissions = [PermissionType]() 23 | // for permission in permissions { 24 | // if permission.getPermissionManager()?.authorizationStatus == 25 | // } 26 | // } 27 | // Based on system API query, independent from memory 28 | static func filterForShouldAskPermission(for permissions: [PermissionManager]) -> [PermissionManager] { 29 | var filteredPermissions = [PermissionManager]() 30 | 31 | for permission in permissions { 32 | if permission.authorizationStatus == .notDetermined { 33 | filteredPermissions.append(permission) 34 | } 35 | } 36 | return filteredPermissions 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIContacts/JMContactsPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMContactsPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | #if !os(tvOS) 10 | import Contacts 11 | import AddressBook 12 | import CorePermissionsSwiftUI 13 | 14 | @available(iOS 13.0, tvOS 13.0, *) 15 | public extension PermissionManager { 16 | ///A permission that allows developers to read & write to device contacts 17 | static let contacts = JMContactsPermissionManager() 18 | } 19 | 20 | @available(iOS 13.0, tvOS 13.0, *) 21 | public final class JMContactsPermissionManager: PermissionManager { 22 | 23 | typealias authorizationStatus = CNAuthorizationStatus 24 | typealias permissionManagerInstance = JMContactsPermissionManager 25 | public override var permissionType: PermissionType { 26 | .contacts 27 | } 28 | 29 | public override var authorizationStatus: AuthorizationStatus { 30 | switch CNContactStore.authorizationStatus(for: .contacts){ 31 | case .authorized: 32 | return .authorized 33 | case .notDetermined: 34 | return .notDetermined 35 | default: 36 | return .denied 37 | } 38 | } 39 | 40 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 41 | let store = CNContactStore() 42 | store.requestAccess(for: .contacts, completionHandler: { (authStatus, error) in 43 | DispatchQueue.main.async { 44 | completion(authStatus, error) 45 | } 46 | }) 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIReminder/JMRemindersPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMRemindersPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | import PermissionsSwiftUIEvent 10 | 11 | #if !os(tvOS) 12 | import EventKit 13 | import CorePermissionsSwiftUI 14 | 15 | @available(iOS 13.0, tvOS 13.0, *) 16 | public extension PermissionManager { 17 | ///Permission that allows app to read & write to device reminder before iOS 17 18 | @available(tvOS, unavailable) 19 | @available(iOS, deprecated, obsoleted: 17.0, message: "iOS 17.0 introduced breaking changes to EventKit APIs. Learn more at https://developer.apple.com/documentation/eventkit/accessing_the_event_store.", renamed: "remindersFull") 20 | static let reminders = JMRemindersPermissionManager(requestedAccessLevel: .legacy) 21 | 22 | ///Permission that allows app to read & write to device reminder 23 | @available(tvOS, unavailable) 24 | static let remindersFull = JMRemindersPermissionManager(requestedAccessLevel: .full) 25 | } 26 | @available(iOS 13.0, tvOS 13.0, *) 27 | public final class JMRemindersPermissionManager: EventPermissionManager { 28 | public override var permissionType: PermissionType { 29 | .reminders 30 | } 31 | 32 | public override var entityType: EKEntityType { 33 | .reminder 34 | } 35 | 36 | public override func requestPermission(completion: @escaping (Bool, Error?)->()) { 37 | if #available(iOS 17.0, *) { 38 | eventStore.requestFullAccessToReminders(completion: completion) 39 | } 40 | else { 41 | requestLegacyPermission(completion) 42 | } 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUITracking/JMTrackingPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMTrackingPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | import AppTrackingTransparency 10 | import AdSupport 11 | import CorePermissionsSwiftUI 12 | 13 | @available(iOS 14, tvOS 14, *) 14 | public extension PermissionManager { 15 | ///In order for app to track user's data across apps and websites, the tracking permission is needed 16 | static let tracking = JMTrackingPermissionManager() 17 | } 18 | 19 | @available(iOS 14, tvOS 14, *) 20 | public class JMTrackingPermissionManager: PermissionManager { 21 | public override var permissionType: PermissionType { 22 | get { 23 | .tracking 24 | } 25 | } 26 | 27 | override public var authorizationStatus: AuthorizationStatus { 28 | switch ATTrackingManager.trackingAuthorizationStatus{ 29 | case .authorized: 30 | return .authorized 31 | case .notDetermined: 32 | return .notDetermined 33 | default: 34 | return .denied 35 | } 36 | } 37 | 38 | public static var advertisingIdentifier:UUID{ 39 | ASIdentifierManager.shared().advertisingIdentifier 40 | } 41 | 42 | 43 | public override func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 44 | ATTrackingManager.requestTrackingAuthorization { status in 45 | switch status { 46 | case .authorized: 47 | completion(true, nil) 48 | case .notDetermined: 49 | break 50 | default: 51 | completion(false, nil) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Modifiers/Public/MainTextModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainTextModifiers.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: Configure Header and Description Texts 11 | @available(iOS 13.0, tvOS 13.0, *) 12 | public extension CustomizableView { 13 | /** 14 | Displays a customized main header text 15 | 16 | The default header text is **Need Permissions** 17 | 18 | - Parameter _: The custom text to change to 19 | */ 20 | 21 | @inlinable func changeHeaderTo(_ text:String) -> some CustomizableView { 22 | store.configStore.mainTexts.headerText = text 23 | return self 24 | } 25 | /** 26 | Displays a customized header description text 27 | 28 | The default header text is: 29 | ``` 30 | In order for you use certain features of this app, you need to give permissions. See description for each permission 31 | ``` 32 | 33 | - Parameter _: The custom text to change to 34 | */ 35 | 36 | @inlinable func changeHeaderDescriptionTo(_ text:String) -> some CustomizableView { 37 | store.configStore.mainTexts.headerDescription = text 38 | return self 39 | } 40 | /** 41 | Displays a customized bottom header description text 42 | 43 | The default bottom header text is: 44 | ``` 45 | Permission are necessary for all the features and functions to work properly. If not allowed, you have to enable permissions in settings 46 | ``` 47 | 48 | - Parameter _: The custom text to change to 49 | */ 50 | 51 | @inlinable func changeBottomDescriptionTo(_ text:String) -> some CustomizableView { 52 | store.configStore.mainTexts.bottomDescription = text 53 | return self 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Dialog-style/DialogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertView.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | //The body view of the alert pop up, child view of AlertMainView 11 | @available(iOS 13.0, *) 12 | @available(tvOS, unavailable, message: "Dialog style permission view is unavailable for tvOS, use modal style instead.") 13 | struct DialogView: View { 14 | @Binding var showAlert: Bool 15 | @EnvironmentObject var store: PermissionStore 16 | @EnvironmentObject var schemaStore: PermissionSchemaStore 17 | 18 | var mainText: MainTexts{store.mainTexts.contentChanged ? store.mainTexts : store.configStore.mainTexts} 19 | var paddingSize: CGFloat { 20 | screenSize.width < 400 ? 20-(1000-screenSize.width)/120 : 20 21 | } 22 | var body: some View { 23 | VStack{ 24 | HeaderView(exitButtonAction: {showAlert = schemaStore.shouldStayInPresentation}, mainText: mainText) 25 | .padding(.bottom, paddingSize/1.5) 26 | PermissionSection(showing: $showAlert) 27 | 28 | if store.permissions.count < 2{ 29 | Divider() 30 | } 31 | Text(mainText.bottomDescription) 32 | .font(.system(.caption, design: .rounded)) 33 | .fontWeight(.regular) 34 | .foregroundColor(Color(.systemGray)) 35 | 36 | .lineLimit(3) 37 | .frame(maxWidth:.infinity, alignment: .leading) 38 | .fixedSize(horizontal: false, vertical: true) 39 | .minimumScaleFactor(0.5) 40 | } 41 | .padding(paddingSize) 42 | .alertViewFrame() 43 | 44 | 45 | 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Modal-style/ModalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalView.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | struct ModalView: View { 12 | @EnvironmentObject var store: PermissionStore 13 | @EnvironmentObject var schemaStore: PermissionSchemaStore 14 | 15 | @Binding var showModal: Bool 16 | var mainText: MainTexts{store.mainTexts.contentChanged ? store.mainTexts : store.configStore.mainTexts} 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack { 21 | HeaderView(exitButtonAction: {showModal = schemaStore.shouldStayInPresentation}, mainText: mainText) 22 | 23 | PermissionSection(showing: $showModal) 24 | .background(Color(.systemBackground)) 25 | .clipShape(RoundedRectangle(cornerRadius: 15)) 26 | .padding() 27 | .frame(maxWidth:UIScreen.main.bounds.width-30) 28 | 29 | Text(.init(mainText.bottomDescription)) 30 | .font(.system(.callout, design: .rounded)) 31 | .foregroundColor(Color(.systemGray)) 32 | .padding(.horizontal) 33 | .textHorizontalAlign(.leading) 34 | 35 | Spacer() 36 | } 37 | .padding(.bottom,30) 38 | 39 | } 40 | .background(Color(.secondarySystemBackground)) 41 | .edgesIgnoringSafeArea(.all) 42 | .introspectViewController{ 43 | if store.configStore.restrictDismissal || 44 | store.restrictAlertDismissal || 45 | store.restrictModalDismissal { 46 | $0.isModalInPresentation = true 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIBluetooth/JMBluetoothPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMBluetoothPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | 8 | import CoreBluetooth 9 | import UIKit 10 | import CorePermissionsSwiftUI 11 | 12 | @available(iOS 13.0, tvOS 13.0, *) 13 | public extension PermissionManager { 14 | ///Permission that allows app to access device's bluetooth technologies 15 | static let bluetooth = JMBluetoothPermissionManager() 16 | } 17 | 18 | @available(iOS 13.0, tvOS 13.0, *) 19 | final public class JMBluetoothPermissionManager: PermissionManager { 20 | private var completion: ((Bool, Error?) -> Void)? 21 | private var manager: CBCentralManager? 22 | 23 | public override var permissionType: PermissionType { 24 | .bluetooth 25 | } 26 | 27 | public override var authorizationStatus: AuthorizationStatus { 28 | switch CBCentralManager().authorization{ 29 | case .allowedAlways: 30 | return .authorized 31 | case .notDetermined: 32 | return .notDetermined 33 | default: 34 | return .denied 35 | } 36 | } 37 | 38 | public override func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 39 | self.completion = completion 40 | self.manager = CBCentralManager(delegate: self, queue: nil) 41 | } 42 | } 43 | 44 | @available(iOS 13.0, tvOS 13.0, *) 45 | extension JMBluetoothPermissionManager: CBCentralManagerDelegate { 46 | public func centralManagerDidUpdateState(_ central: CBCentralManager) { 47 | switch central.authorization { 48 | case .notDetermined: 49 | break 50 | case .allowedAlways: 51 | self.completion?(true, nil) 52 | default: 53 | self.completion?(false, nil) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIMotion/JMMotionPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMMotionPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | #if !os(tvOS) 10 | import CoreMotion 11 | import CorePermissionsSwiftUI 12 | 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | ///Permission that give app access to motion and fitness related sensor data 16 | static let motion = JMMotionPermissionManager() 17 | } 18 | 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public final class JMMotionPermissionManager: PermissionManager { 21 | 22 | typealias authorizationStatus = CMAuthorizationStatus 23 | typealias permissionManagerInstance = JMMotionPermissionManager 24 | public override var permissionType: PermissionType { 25 | .motion 26 | } 27 | 28 | public override var authorizationStatus: AuthorizationStatus { 29 | switch CMMotionActivityManager.authorizationStatus() { 30 | case .authorized: 31 | return .authorized 32 | case .notDetermined: 33 | return .notDetermined 34 | default: 35 | return .denied 36 | } 37 | } 38 | 39 | 40 | 41 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 42 | let manager = CMMotionActivityManager() 43 | let today = Date() 44 | 45 | manager.queryActivityStarting(from: today, to: today, to: OperationQueue.main, withHandler: { (activities: [CMMotionActivity]?, error: Error?) -> () in 46 | if error != nil { 47 | completion(false, error) 48 | } 49 | else{ 50 | completion(true, nil) 51 | } 52 | manager.stopActivityUpdates() 53 | }) 54 | 55 | 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIEvent/EventPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventPermissionManager.swift 3 | // PermissionsSwiftUI-Example 4 | // 5 | // Created by Jevon Mao on 8/26/23. 6 | // 7 | 8 | import Foundation 9 | import CorePermissionsSwiftUI 10 | 11 | #if !os(tvOS) 12 | import EventKit 13 | 14 | open class EventPermissionManager: PermissionManager { 15 | public init(requestedAccessLevel: AccessLevel = .legacy) { 16 | self.requestedAccessLevel = requestedAccessLevel 17 | if requestedAccessLevel == .legacy { 18 | NSLog("[PermissionsSwiftUI]: WARNING! Using legacy calendar or reminder permission, which will NOT work in iOS 17 and always return denied due to Apple EventKit API changes. Learn more: https://developer.apple.com/documentation/eventkit/accessing_the_event_store") 19 | } 20 | } 21 | 22 | 23 | public var requestedAccessLevel: AccessLevel 24 | public let eventStore = EKEventStore() 25 | open var entityType: EKEntityType { 26 | get { 27 | preconditionFailure("This property must be overridden.") 28 | } 29 | } 30 | 31 | public enum AccessLevel { 32 | case writeOnly 33 | case full 34 | case legacy 35 | } 36 | 37 | public override var authorizationStatus: AuthorizationStatus { 38 | switch EKEventStore.authorizationStatus(for: entityType){ 39 | case .authorized: 40 | return .authorized 41 | case .notDetermined: 42 | return .notDetermined 43 | default: 44 | return .denied 45 | } 46 | } 47 | 48 | public func requestLegacyPermission( _ completion: @escaping (Bool, Error?) -> Void) { 49 | eventStore.requestAccess(to: entityType, completion: { 50 | (accessGranted: Bool, error: Error?) in 51 | DispatchQueue.main.async { 52 | completion(accessGranted, error) 53 | } 54 | }) 55 | } 56 | 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUI/API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 6/12/21. 6 | // 7 | 8 | import CorePermissionsSwiftUI 9 | import PermissionsSwiftUITracking 10 | import PermissionsSwiftUIBluetooth 11 | import PermissionsSwiftUICalendar 12 | import PermissionsSwiftUICamera 13 | import PermissionsSwiftUIContacts 14 | import PermissionsSwiftUIHealth 15 | import PermissionsSwiftUILocation 16 | import PermissionsSwiftUILocationAlways 17 | import PermissionsSwiftUIMicrophone 18 | import PermissionsSwiftUIMotion 19 | import PermissionsSwiftUIMusic 20 | import PermissionsSwiftUINotification 21 | import PermissionsSwiftUIPhoto 22 | import PermissionsSwiftUIReminder 23 | import PermissionsSwiftUISpeech 24 | 25 | @available(iOS 13.0, tvOS 13.0, *) 26 | public extension Array where Element == PermissionManager { 27 | 28 | /** 29 | Get all the permission managers in an array 30 | 31 | The `allCases` property extending the Array type will return all PermissionsSwiftUI's supported permission managers, in an array of `PermissionManager`. A common use case of this property would be showcasing all permissions, or for debugging purposes. 32 | */ 33 | static var allCases: [PermissionManager] { 34 | #if !os(tvOS) 35 | if #available(iOS 14, *) { 36 | return [.location,.locationAlways,.photo, .microphone,.camera,.notification,.calendar,.bluetooth,.contacts,.motion,.reminders,.speech,.tracking,.health(categories: .init())] 37 | } else { 38 | return [.location,.locationAlways,.photo,.microphone,.camera,.notification,.calendar,.bluetooth,.contacts,.motion,.reminders,.speech,.health(categories: .init())] 39 | } 40 | #else 41 | if #available(tvOS 14, *) { 42 | return [.location, .photo, .notification, .bluetooth, .tracking] 43 | } 44 | else { 45 | return [.location, .photo, .notification, .bluetooth] 46 | } 47 | #endif 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Shared/ExitButtonSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExitButtonSection.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | struct ExitButtonSection: View { 12 | //Action is closing the alert or modal on tap 13 | var action: () -> Void 14 | var buttonSizeConstant: CGFloat { 15 | screenSize.width < 400 ? 40-(1000-screenSize.width)/80 : 40 16 | } 17 | @EnvironmentObject var schemaStore: PermissionSchemaStore 18 | var body: some View { 19 | Button(action: { 20 | #if !os(tvOS) 21 | let haptics = HapticsManager() 22 | if schemaStore.shouldStayInPresentation { 23 | haptics.notificationImpact(.error) 24 | 25 | } 26 | #endif 27 | action() 28 | 29 | }, label: { 30 | 31 | // show button if dismissial is allowed && autoDismiss is disabled ( restrictDismissal == false, autoDismiss == false) 32 | // otherwise, only show the button except if the user fully interacted with all permission prompts ( shouldStayInPresentation ) 33 | 34 | if (schemaStore.store.configStore.restrictDismissal == false && schemaStore.store.configStore.autoDismiss == false) || schemaStore.shouldStayInPresentation == false { 35 | 36 | Circle() 37 | .fill(Color(.systemGray4)) 38 | .frame(width: buttonSizeConstant, height: buttonSizeConstant) 39 | .overlay( 40 | Image(systemName: "xmark") 41 | .font(.system(size: 18, weight: .bold, design: .rounded)) 42 | .minimumScaleFactor(0.2) 43 | .foregroundColor(Color(.systemGray)) 44 | .padding(4) 45 | ) 46 | } 47 | }) 48 | .accessibility(identifier: "Exit button") 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Utility/Extensions/ColorsAvailability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 4/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor{ 11 | static var systemGray2: UIColor { 12 | get { 13 | if #available(iOS 13.0, *) { 14 | return UIColor(red: 0.68, green: 0.68, blue: 0.7, alpha: 1) 15 | } 16 | return UIColor.clear 17 | } 18 | } 19 | static var systemGray4: UIColor { 20 | get { 21 | if #available(iOS 13.0, *) { 22 | return UIColor(red: 0.82, green: 0.82, blue: 0.84, alpha: 1) 23 | } 24 | return UIColor.clear 25 | 26 | } 27 | } 28 | static var systemGray5: UIColor { 29 | get { 30 | if #available(iOS 13.0, *) { 31 | return UIColor(red: 0.9, green: 0.9, blue: 0.92, alpha: 1) 32 | } 33 | return UIColor.clear 34 | } 35 | } 36 | static var secondarySystemBackground: UIColor { 37 | get { 38 | if #available(tvOS 13.0, iOS 13.0, *) { 39 | return UIColor { (traits) -> UIColor in 40 | return traits.userInterfaceStyle == .dark ? 41 | UIColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1) : 42 | UIColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1) 43 | } 44 | } else { 45 | return UIColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1) 46 | } 47 | } 48 | } 49 | static var systemBackground: UIColor { 50 | get { 51 | if #available(tvOS 13.0, iOS 13.0, *) { 52 | return UIColor { (traits) -> UIColor in 53 | return traits.userInterfaceStyle == .dark ? 54 | UIColor(red: 0, green: 0, blue: 0, alpha: 1) : 55 | UIColor(red: 1, green: 1, blue: 1, alpha: 1) 56 | } 57 | } else { 58 | return UIColor(red: 1, green: 1, blue: 1, alpha: 1) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build_iOS: 11 | runs-on: macos-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v2 15 | - name: Setup Xcode 16 | uses: maxim-lobanov/setup-xcode@v1 17 | with: 18 | xcode-version: latest-stable 19 | - name: Build 20 | run: xcodebuild -scheme PermissionsSwiftUI -destination 'generic/platform=iOS' build 21 | # test_iOS: 22 | # runs-on: macos-latest 23 | # steps: 24 | # - name: Checkout Repo 25 | # uses: actions/checkout@v2 26 | # - name: Setup Xcode 27 | # uses: maxim-lobanov/setup-xcode@v1 28 | # with: 29 | # xcode-version: latest-stable 30 | # - name: Test large screen device 31 | # run: xcodebuild -scheme PermissionsSwiftUI -destination 'platform=iOS Simulator,name=iPhone 12 Pro Max' -only-testing:PermissionsSwiftUITests test 32 | # - name: Test small screen device 33 | # run: xcodebuild -scheme PermissionsSwiftUI -destination 'platform=iOS Simulator,name=iPod touch (7th generation)' -only-testing:PermissionsSwiftUISmallScreenTests test 34 | Swift_format_lint: 35 | name: Check swiftformat 36 | runs-on: macos-latest 37 | steps: 38 | - name: Checkout repo 39 | uses: actions/checkout@v2 40 | 41 | - name: Install Swiftformat 42 | run: brew install swiftformat 43 | 44 | - name: Format Swift code 45 | run: swiftformat --verbose . --swiftversion 5.2 46 | documentation_coverage: 47 | name: Documentation coverage 48 | runs-on: macos-latest 49 | steps: 50 | - name: Checkout repo 51 | uses: actions/checkout@v2 52 | - name: Install Jazzy 53 | run: sudo gem install jazzy 54 | - name: Ready shell script 55 | run: chmod +x .github/documentation_coverage.sh 56 | - name: Execute shell script 57 | run: .github/documentation_coverage.sh 58 | # lint_podspec: 59 | # name: Lint podspec 60 | # runs-on: macos-latest 61 | # steps: 62 | # - name: Checkout Repo 63 | # uses: actions/checkout@v2 64 | # - name: lint podspec 65 | # run: pod lib lint --allow-warnings 66 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUINotification/JMNotificationPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import UserNotifications 11 | import CorePermissionsSwiftUI 12 | 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | ///The `notification` permission allows the iOS system to receive notification from app 16 | static let notification = JMNotificationPermissionManager() 17 | } 18 | 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public final class JMNotificationPermissionManager: PermissionManager { 21 | 22 | 23 | public override var permissionType: PermissionType { 24 | .notification 25 | } 26 | 27 | public override var authorizationStatus: AuthorizationStatus { 28 | var notificationSettings: UNNotificationSettings? 29 | let semaphore = DispatchSemaphore(value: 0) 30 | 31 | DispatchQueue.global().async { 32 | self.notificationManager.getNotificationSettings { settings in 33 | notificationSettings = settings 34 | semaphore.signal() 35 | } 36 | } 37 | 38 | semaphore.wait() 39 | guard let settings = notificationSettings else{ 40 | #if DEBUG 41 | print("Notification settings is nil while getting authorization status for JMNotificationPermissionManager") 42 | #endif 43 | return .notDetermined 44 | } 45 | switch settings.authorizationStatus{ 46 | case .authorized: 47 | return .authorized 48 | case .denied: 49 | return .denied 50 | case .notDetermined: 51 | return .notDetermined 52 | case .provisional: 53 | return .limited 54 | default: 55 | return .denied 56 | } 57 | } 58 | var notificationManager = UNUserNotificationCenter.current() 59 | 60 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 61 | notificationManager.requestAuthorization(options: [.badge,.alert,.sound]){ granted, error in 62 | completion(granted, error) 63 | 64 | } 65 | 66 | UIApplication.shared.registerForRemoteNotifications() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/PermissionManagers/PermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionManager.swift 3 | // PermissionsSwiftUI-Example 4 | // 5 | // Created by Jevon Mao on 8/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A Permission Manager object that contains properties and functions related to a specific permission. Will be subclassed by any permission type. 12 | 13 | - warning: `PermissionManager` shoud never be referenced directly and used. It serves as an abstract interface for PermissionsSwiftUI's many permission modules. 14 | */ 15 | open class PermissionManager: NSObject, Identifiable { 16 | ///Holds the permission UI component, containing UI elements like text and image 17 | open var permissionComponent: JMPermission { 18 | get { 19 | preconditionFailure("This property must be overridden.") 20 | } 21 | } 22 | ///The type of permission 23 | open var permissionType: PermissionType { 24 | preconditionFailure("This property must be overridden.") 25 | } 26 | 27 | ///The authorization status of the permission 28 | open var authorizationStatus: AuthorizationStatus { 29 | get { 30 | preconditionFailure("This property must be overridden.") 31 | } 32 | } 33 | 34 | #if PERMISSIONSWIFTUI_HEALTH 35 | 36 | ///Holds the health permission subcategories, in case of health permission type subclass 37 | open var healthPermissionCategories: Set? 38 | 39 | /** 40 | Creates a new `PermissionManager` for health permission. 41 | 42 | - parameters: 43 | - healthPermissionCategories: Subcategory permissions of health permission to request 44 | */ 45 | public init(_ healthPermissionCategories: Set? = nil) { 46 | self.healthPermissionCategories = healthPermissionCategories 47 | } 48 | #else 49 | ///Creates a new `PermissionManager` for any type of child implemented permission 50 | public override init() {} 51 | #endif 52 | 53 | /** 54 | Requests authorization for the current implemented type of permission. 55 | 56 | - parameters: 57 | - completion: Returns back whether the permission authorization is granted, and any errors 58 | */ 59 | open func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 60 | preconditionFailure("This method must be overridden.") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/PermissionSchemaStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/25/21. 6 | // 7 | 8 | import Combine 9 | 10 | /** 11 | The schema storage class that coordinates PermissionsSwiftUI's internal functions 12 | */ 13 | 14 | @available(iOS 13.0, tvOS 13.0, *) 15 | public class PermissionSchemaStore: ObservableObject { 16 | 17 | //MARK: Filtered permission arrays 18 | var undeterminedPermissions: [PermissionManager] { 19 | FilterPermissions.filterForShouldAskPermission(for: permissions) 20 | } 21 | var interactedPermissions: [PermissionManager] { 22 | //Filter for permissions that are not interacted 23 | permissions.filter { 24 | permissionComponentsStore.getPermissionComponent(for: $0.permissionType).interacted 25 | } 26 | } 27 | var successfulPermissions: [JMResult]? 28 | var erroneousPermissions: [JMResult]? 29 | 30 | //MARK: Controls dismiss restriction 31 | var shouldStayInPresentation: Bool { 32 | if configStore.restrictDismissal || 33 | ((permissionViewStyle == .modal && store.restrictModalDismissal) || 34 | (permissionViewStyle == .alert && store.restrictAlertDismissal)) { 35 | // number of interacted permissions equal to number 36 | // of all permissions means means everything has been 37 | // interacted with, thus if so, shouldStayInPresentation 38 | // will be false and dismissal is allowed 39 | return !(interactedPermissions.count == permissions.count) 40 | } 41 | return false 42 | } 43 | //MARK: Initialized configuration properties 44 | var configStore: ConfigStore 45 | var store: PermissionStore 46 | @Published var permissions: [PermissionManager] 47 | var permissionViewStyle: PermissionViewStyle 48 | @usableFromInline var permissionComponentsStore: PermissionComponentsStore 49 | init(store: PermissionStore, permissionViewStyle: PermissionViewStyle) { 50 | self.configStore = store.configStore 51 | self.permissions = store.permissions 52 | self.permissionComponentsStore = store.permissionComponentsStore 53 | self.store = store 54 | self.permissionViewStyle = permissionViewStyle 55 | } 56 | 57 | } 58 | 59 | @usableFromInline enum PermissionViewStyle { 60 | case alert, modal 61 | } 62 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Mocks/MockNotificationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNotificationManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import Foundation 9 | #if PERMISSIONSWIFTUI_NOTIFICATION 10 | import UserNotifications 11 | 12 | protocol NotificationManager { 13 | static func shared() -> NotificationManager 14 | func requestPermission(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) 15 | func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Void) 16 | } 17 | extension UNUserNotificationCenter: NotificationManager{ 18 | static var isTestingMode = false 19 | static func shared() -> NotificationManager { 20 | return isTestingMode ? MockNotificationManager.shared() : UNUserNotificationCenter.current() 21 | } 22 | func requestPermission(options: UNAuthorizationOptions=[], completionHandler: @escaping (Bool, Error?) -> Void) { 23 | self.requestAuthorization(options: options) { granted, error in 24 | completionHandler(granted,error) 25 | } 26 | } 27 | 28 | } 29 | final class MockNotificationManager:NotificationManager{ 30 | var authStatus:UNAuthorizationStatus = .notDetermined 31 | func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Void) { 32 | completionHandler(UNNotificationSettings(coder: MockNSCoder(authorizationStatus: authStatus.rawValue))!) 33 | 34 | } 35 | 36 | static func shared() -> NotificationManager { 37 | MockNotificationManager() 38 | } 39 | 40 | 41 | func requestPermission(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) { 42 | switch authStatus{ 43 | case .denied: 44 | completionHandler(false,NSError(domain:"", code:0, userInfo:nil)) 45 | case .ephemeral: 46 | completionHandler(false,nil) 47 | default: 48 | completionHandler(true,nil) 49 | authStatus = .authorized 50 | } 51 | } 52 | 53 | 54 | } 55 | final class MockNSCoder: NSCoder { 56 | var authorizationStatus:Int 57 | init(authorizationStatus:Int){ 58 | self.authorizationStatus = authorizationStatus 59 | } 60 | override func decodeInt64(forKey key: String) -> Int64 { 61 | return Int64(authorizationStatus) 62 | } 63 | 64 | override func decodeBool(forKey key: String) -> Bool { 65 | return true 66 | } 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIHealth/HKAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HKAccess.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/17/21. 6 | // 7 | 8 | import Foundation 9 | #if !os(tvOS) && PERMISSIONSWIFTUI_HEALTH 10 | import HealthKit 11 | /** 12 | Encapsulates different subtypes of permission for health permission 13 | 14 | The structure `HKAccess` is required when initalizing health permission's enum associated values. It encapsulates the read and write type permissions for the health permission. 15 | */ 16 | public struct HKAccess: Hashable { 17 | ///The HealthKit sample types for read permission 18 | public var readPermissions: Set = Set() 19 | ///The HealthKit sample types for write permission 20 | public var writePermissions: Set = Set() 21 | 22 | /** 23 | Initializes a new `HKAccess` with separate read and write permissions 24 | 25 | - parameters: 26 | - read: The HealthKit sample types for read permission 27 | - write: The HealthKit sample types for write permission 28 | */ 29 | public init(read: Set, write: Set){ 30 | self.readPermissions = read 31 | self.writePermissions = write 32 | } 33 | /** 34 | Initializes a new `HKAccess` with read permissions 35 | 36 | - parameters: 37 | - read: The HealthKit sample types for read permission 38 | - write: The HealthKit sample types for write permission 39 | */ 40 | public init(read: Set){ 41 | self.readPermissions = read 42 | } 43 | /** 44 | Initializes a new `HKAccess` with write permissions 45 | 46 | - parameters: 47 | - read: The HealthKit sample types for read permission 48 | - write: The HealthKit sample types for write permission 49 | */ 50 | public init(write: Set){ 51 | self.writePermissions = write 52 | } 53 | /** 54 | Initializes a new `HKAccess` with empty read and write permissions 55 | 56 | - warning: This initializer should never be used in production. It will definitely crash the application when health permission is requested. 57 | */ 58 | public init(){} 59 | } 60 | 61 | extension HKAccess { 62 | 63 | /** 64 | Initializes a new `HKAccess` with same read and write permissions 65 | 66 | - parameters: 67 | - readAndWrite: sample types for read permission 68 | */ 69 | public init(readAndWrite sharedPermissions: Set){ 70 | self.init(read: sharedPermissions, write: sharedPermissions) 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUICalendar/JMCalendarPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMCalendarPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | 8 | import Foundation 9 | import PermissionsSwiftUIEvent 10 | 11 | #if !os(tvOS) 12 | import EventKit 13 | import CorePermissionsSwiftUI 14 | 15 | @available(iOS 13.0, tvOS 13.0, *) 16 | public extension PermissionManager { 17 | ///Permission that allows app to read & write to device calendar 18 | @available(tvOS, unavailable) 19 | static let calendarFull = JMCalendarPermissionManager(requestedAccessLevel: .full) 20 | 21 | ///Permission that allows app to only write to device calendar 22 | @available(tvOS, unavailable) 23 | static let calenderWrite = JMCalendarPermissionManager(requestedAccessLevel: .writeOnly) 24 | 25 | ///Permission that allows app to read & write to device calendar before iOS 17 26 | @available(tvOS, unavailable) 27 | @available(iOS, deprecated, obsoleted: 17.0, message: "iOS 17.0 introduced breaking changes to EventKit APIs, use 'calendarFull' or 'calendarWrite' instead. Learn more at https://developer.apple.com/documentation/eventkit/accessing_the_event_store.") 28 | static let calendar = JMCalendarPermissionManager(requestedAccessLevel: .legacy) 29 | } 30 | 31 | @available(iOS 13.0, tvOS 13.0, *) 32 | public final class JMCalendarPermissionManager: EventPermissionManager { 33 | public override var permissionType: PermissionType { 34 | .calendar 35 | } 36 | 37 | public override var entityType: EKEntityType { 38 | .event 39 | } 40 | 41 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 42 | switch requestedAccessLevel { 43 | case .legacy: 44 | requestLegacyPermission(completion) 45 | case .full: 46 | if #available(iOS 17.0, *) { 47 | eventStore.requestFullAccessToEvents{(success, error) in 48 | DispatchQueue.main.async { 49 | completion(success, error) 50 | } 51 | 52 | } 53 | } else { 54 | requestLegacyPermission(completion) 55 | } 56 | case .writeOnly: 57 | if #available(iOS 17.0, *) { 58 | eventStore.requestWriteOnlyAccessToEvents {(success, error) in 59 | DispatchQueue.main.async { 60 | completion(success, error) 61 | } 62 | } 63 | } else { 64 | requestLegacyPermission(completion) 65 | } 66 | } 67 | } 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Structs/JMPermission.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMPermission.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | /** 11 | A data model that defines a JPPermission component and its data 12 | 13 | `JMPermission` conforms to `Equatable` and `Hashable`. The PermissionsSwiftUI view interface will be rendered based on information from `JMPermission` structure 14 | */ 15 | @available(iOS 13.0, tvOS 13.0, *) 16 | public struct JMPermission: Equatable { 17 | 18 | /** 19 | Initializes a new instance of `JMPermission` that encapsulates properties of a single permission 20 | 21 | - Parameters: 22 | - imageIcon: Defines the image icon displayed for the permission component 23 | - title: Defines the title text of the permission component 24 | - description: Defines the description text of the permission component 25 | - authorized: Tracks the authorization status of ther permission 26 | */ 27 | public init(imageIcon: AnyView, title: String, description: String) { 28 | self.imageIcon = imageIcon 29 | self.title = title 30 | self.description = description 31 | } 32 | @usableFromInline internal init(imageIcon: AnyView, title: String, description: String, authorized: Bool) { 33 | self.imageIcon = imageIcon 34 | self.title = title 35 | self.description = description 36 | self.authorized = authorized 37 | } 38 | /** 39 | Compares two instances of equatable `JMPermission`, based on the title text, description text, and authorized status 40 | 41 | `imageIcon` will be ignored by the comparison operature, because it is a type erased `AnyView` and cannot be compared 42 | 43 | - Parameters: 44 | - imageIcon: Defines the image icon displayed for the permission component 45 | - title: Defines the title text of the permission component 46 | - description: Defines the description text of the permission component 47 | - authorized: Tracks the authorization status of ther permission 48 | */ 49 | public static func == (lhs: JMPermission, rhs: JMPermission) -> Bool { 50 | if lhs.title == rhs.title && lhs.description == rhs.description && lhs.authorized == rhs.authorized{ 51 | return true 52 | } 53 | else{ 54 | return false 55 | } 56 | } 57 | ///The image icon displayed 58 | public var imageIcon: AnyView 59 | ///The permission name displayed 60 | public var title: String 61 | ///The permission description displayed 62 | public var description: String 63 | @usableFromInline internal var authorized:Bool = false 64 | internal var interacted:Bool = false 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUILocation/JMLocationPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMLocationInUsePermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/18/21. 6 | // 7 | 8 | 9 | import Foundation 10 | import MapKit 11 | import CorePermissionsSwiftUI 12 | 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | ///The `location` permission allows the device's positoin to be tracked 16 | static let location = JMLocationPermissionManager() 17 | } 18 | 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public final class JMLocationPermissionManager: PermissionManager, CLLocationManagerDelegate { 21 | typealias authorizationStatus = CLAuthorizationStatus 22 | typealias permissionManagerInstance = JMLocationPermissionManager 23 | 24 | 25 | public override var permissionType: PermissionType { 26 | .location 27 | } 28 | 29 | public override var authorizationStatus: AuthorizationStatus { 30 | switch CLLocationManager.authorizationStatus(){ 31 | case .authorizedAlways: 32 | return .authorized 33 | case .authorizedWhenInUse: 34 | return .authorized 35 | case .notDetermined: 36 | return .notDetermined 37 | default: 38 | return .denied 39 | } 40 | } 41 | 42 | var completionHandler: ((Bool, Error?) -> Void)? 43 | var locationManager = CLLocationManager() 44 | 45 | public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 46 | if status == .notDetermined { 47 | return 48 | } 49 | 50 | if let completionHandler = completionHandler { 51 | let status = CLLocationManager.authorizationStatus() 52 | 53 | //Completion handler called from this delegate function 54 | //Both authorizedAlways and authorizedWhenInUse will be acceptable 55 | completionHandler(status == .authorizedAlways || status == .authorizedWhenInUse ? true : false, nil) 56 | 57 | } 58 | } 59 | //Used to request in use permission (1 of 2 types of iOS location permission) 60 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 61 | self.completionHandler = completion 62 | let status = CLLocationManager.authorizationStatus() 63 | 64 | switch status { 65 | case .notDetermined: 66 | self.locationManager.delegate = self 67 | self.locationManager.requestWhenInUseAuthorization() 68 | default: 69 | completion(status == .authorizedWhenInUse || status == .authorizedAlways ? true : false, nil) 70 | } 71 | } 72 | 73 | deinit { 74 | locationManager.delegate = nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUILocationAlways/JMLocationAlwaysPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMLocationPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | import CorePermissionsSwiftUI 11 | 12 | #if !os(tvOS) 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | ///The `locationAlways` permission provides location data even if app is in background 16 | static let locationAlways = JMLocationAlwaysPermissionManager() 17 | } 18 | 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public final class JMLocationAlwaysPermissionManager: PermissionManager, CLLocationManagerDelegate { 21 | 22 | 23 | typealias authorizationStatus = CLAuthorizationStatus 24 | typealias permissionManagerInstance = JMLocationAlwaysPermissionManager 25 | public override var permissionType: PermissionType { 26 | .locationAlways 27 | } 28 | public override var authorizationStatus: AuthorizationStatus { 29 | switch CLLocationManager.authorizationStatus(){ 30 | case .authorizedAlways: 31 | return .authorized 32 | case .notDetermined: 33 | return .notDetermined 34 | default: 35 | //In use only permission will be counted as denied 36 | return .denied 37 | } 38 | 39 | } 40 | 41 | var locationManager = CLLocationManager() 42 | //Completion block for is authorized or not authorized 43 | var completionHandler: ((Bool, Error?) -> Void)? 44 | 45 | //CLLocationManagerDelegate method triggered when user approve or deny permission 46 | public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 47 | if status == .notDetermined { 48 | return 49 | } 50 | 51 | if let completionHandler = completionHandler { 52 | let status = CLLocationManager.authorizationStatus() 53 | completionHandler(status == .authorizedAlways ? true : false, nil) 54 | } 55 | } 56 | 57 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 58 | self.completionHandler = completion 59 | var status:CLAuthorizationStatus{ 60 | CLLocationManager.authorizationStatus() 61 | } 62 | 63 | switch status { 64 | case .notDetermined: 65 | self.locationManager.delegate = self 66 | self.locationManager.requestAlwaysAuthorization() 67 | case .authorizedWhenInUse: 68 | self.locationManager.delegate = self 69 | self.locationManager.requestAlwaysAuthorization() 70 | default: 71 | if let completionHandler = completionHandler { 72 | completionHandler(status == .authorizedAlways ? true : false, nil) 73 | } 74 | } 75 | } 76 | 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ### Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | Using welcoming and inclusive language 12 | Being respectful of differing viewpoints and experiences 13 | Gracefully accepting constructive criticism 14 | Focusing on what is best for the community 15 | Showing empathy towards other community members 16 | Examples of unacceptable behavior by participants include: 17 | 18 | The use of sexualized language or imagery and unwelcome sexual attention or advances 19 | Trolling, insulting/derogatory comments, and personal or political attacks 20 | Public or private harassment 21 | Publishing others' private information, such as a physical or electronic address, without explicit permission 22 | Other conduct which could reasonably be considered inappropriate in a professional setting 23 | Our Responsibilities 24 | 25 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 26 | 27 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 28 | 29 | ### Scope 30 | 31 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 32 | 33 | ### Enforcement 34 | 35 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [woodburyjevonmao@gmail.com]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 36 | 37 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 38 | 39 | ### Attribution 40 | 41 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 42 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Dialog-style/DialogViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertMainView.swift.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | //The root level view for alert-style 11 | @available(iOS 13.0, *) 12 | @available(tvOS, unavailable, message: "Dialog style permission view is unavailable for tvOS, use modal style instead.") 13 | @usableFromInline struct DialogViewWrapper: View, CustomizableView { 14 | @usableFromInline typealias ViewType = Body 15 | @usableFromInline var showing: Binding 16 | @usableFromInline var bodyView: ViewType 17 | @usableFromInline var store: PermissionStore 18 | @usableFromInline var schemaStore: PermissionSchemaStore 19 | init(for bodyView: ViewType, showing: Binding, store: PermissionStore) { 20 | self.showing = showing 21 | self.bodyView = bodyView 22 | self.store = store 23 | self.schemaStore = PermissionSchemaStore(store: store, 24 | permissionViewStyle: .alert) 25 | } 26 | var shouldShowPermission:Bool{ 27 | //Handles case where configuration for autoCheckAuth is true 28 | if store.configStore.autoCheckAuth || store.autoCheckAlertAuth { 29 | if showing.wrappedValue && 30 | //schemaStore underterminedPermissions (askable permissions) must not be empty 31 | !schemaStore.undeterminedPermissions.isEmpty { 32 | return true 33 | } 34 | else { 35 | return false 36 | } 37 | } 38 | if showing.wrappedValue{ 39 | return true 40 | } 41 | else { 42 | return false 43 | } 44 | } 45 | @usableFromInline var body: some View { 46 | ZStack{ 47 | let insertTransition = AnyTransition.opacity.combined(with: .scale(scale: 1.1)).animation(Animation.default.speed(1.6)) 48 | let removalTransiton = AnyTransition.opacity.combined(with: .scale(scale: 0.9)).animation(Animation.default.speed(1.8)) 49 | bodyView 50 | if shouldShowPermission { 51 | Group{ 52 | #if !os(tvOS) 53 | Blur(style: .systemUltraThinMaterialDark) 54 | .transition(AnyTransition.opacity.animation(Animation.default.speed(1.6))) 55 | #else 56 | Blur(style: .extraDark) 57 | .transition(AnyTransition.opacity.animation(Animation.default.speed(1.6))) 58 | #endif 59 | DialogView(showAlert: showing) 60 | .onAppear(perform: store.onAppear ?? store.configStore.onAppear) 61 | .onDisappear(perform: store.onDisappear ?? store.configStore.onDisappear) 62 | 63 | } 64 | .transition(.asymmetric(insertion: insertTransition, removal: removalTransiton)) 65 | .edgesIgnoringSafeArea(.all) 66 | .animation(.default) 67 | 68 | } 69 | }.withEnvironmentObjects(store: store, permissionStyle: .alert) 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/ConfigStore/ConfigStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigStore.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/18/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /** 12 | The data storage class that contains configurable settings 13 | */ 14 | @available(iOS 13.0, tvOS 13.0, *) 15 | public struct ConfigStore { 16 | //MARK: Creating a new configuration store 17 | ///Creates a new configuration store with default settings 18 | public init(){} 19 | //MARK: Configuring View Texts 20 | ///The text for text label components, including header and descriptions 21 | public var mainTexts = MainTexts() 22 | 23 | //MARK: Customizing Colors 24 | ///The color configuration for permission allow buttons 25 | public var allButtonColors = AllButtonColors() 26 | 27 | //MARK: Change Auto Dismiss Behaviors 28 | ///Whether to auto dismiss the view after last permission is allowed 29 | public var autoDismiss: Bool = false 30 | 31 | //MARK: Configure Auto Authorization Checking 32 | ///Whether to auto check for authorization status before showing, and show the view only if permission is in `notDetermined` 33 | public var autoCheckAuth: Bool = true 34 | 35 | //MARK: Prevent Dismissal Before All Permissions Interacted 36 | ///Whether to prevent dismissal of view before all permissions have been interacted (explict deny or allow) 37 | public var restrictDismissal: Bool = true 38 | 39 | //MARK: Configure permission description text color 40 | public var permissionDescriptionForeground: any ShapeStyle = Color(.systemGray2) 41 | 42 | //MARK: `onAppear` and `onDisappear` Executions 43 | ///Override point for executing action when PermissionsSwiftUI view appears 44 | public var onAppear: (()->Void)? 45 | ///Override point for executing action when PermissionsSwiftUI view disappears 46 | public var onDisappear: (()->Void)? 47 | /** 48 | Called when PermissionsSwiftUI view disappears with additional parameters that encapsulates the results 49 | 50 | This completion handler will return additional details about the permission request's results, and execute action when PermissionsSwiftUI view disappears. 51 | */ 52 | public var onDisappearHandler: ((_ successful: [JMResult]?, _ erroneous: [JMResult]?)->Void)? 53 | } 54 | 55 | /** 56 | Encapsulates the surrounding texts and title 57 | */ 58 | public struct MainTexts: Equatable { 59 | //Whether the text properties have been changed and customized 60 | var contentChanged: Bool { 61 | //Represents the default, unchanged struct with default property values 62 | let mainTexts = MainTexts() 63 | if self == mainTexts{return false} 64 | return true 65 | } 66 | ///Text to display for header text 67 | public var headerText: String = "permission_header".localized 68 | ///Text to display for header description text 69 | public var headerDescription: String = "permission_primary_label".localized 70 | ///Text to display for bottom part description text 71 | public var bottomDescription: String = "permission_secondary_label".localized 72 | ///Whether to use the alternative "NEXT" in place of "ALLOW" for the allow button label 73 | public var useAltButtonLabel: Bool = false 74 | } 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by opening a new issue. 4 | 5 | Please note we have a code of conduct, please follow it in all your interactions with the project. 6 | 7 | ## Owner 8 | 9 | PermissionsSwiftUI is created by **Jingwen (Jevon) Mao** 10 | 11 | The current owner is **Jingwen (Jevon) Mao** 12 | 13 | Email: jingwenmao@g.ucla.edu 14 | 15 | Discord: Jevon#2448 16 | 17 | ## Pull Request Process 18 | 19 | 1. Find an issue to work on 20 | 21 | 2. If you want, you can open a new issue or email me about a feature or bug fix 22 | 23 | 3. Fork and clone this repository 24 | 25 | 4. Make a new branch and name it with issue number + short description (ex. #23-feature-new-color) 26 | 27 | 5. DOCUMENT all new APIs, and write code comment if needed 28 | 29 | 6. Open a pull request 30 | 31 | 7. If needed, update the README.md with details of changes of APIs 32 | 33 | ### Branch Protection 34 | We have branch protection in place for main branch, I will review and merge your PR if all CI checks have passed. The CI checks are automatically triggered on pull request or push to main branch, and may include: build, unit tests, format linting, documentation coverage, and Cocoapods linting. 35 | 36 | ## Label Meanings 37 | 38 | PermissionsSwiftUI community uses several different tags to efficiently categorize and manage issues. 39 | 40 | #### `work in progress` 41 | Any issue labeled with this tag means that it is already work in progress, either by the owner or other contributors. While discussion and comments are still welcome, it usually means that this issue is not open for contributing. 42 | 43 | #### `possible bug` 44 | Issue opened with bug template will automatically be labeled with the `possible bug` tag. A possible bug that has not been reviewed by a maintainer or the owner, and will not yet be worked on. 45 | 46 | #### `confirmed bug` 47 | A `confirmed bug` label means that the bug has been reviewed and confirmed by maintainers, and will be worked on to be fixed. 48 | 49 | #### `feature request` 50 | Issue opened with the feature template will automatically be labeled with this tag. It is a request, or a pitch, for a new feature, but the request has not been reviewed by a maintainer or the owner yet. 51 | 52 | #### `enhancement` 53 | This tag is an evolution of the `feature request` tag. The feature request has been reviewed and confirmed by maintainers, and will be implemented in future versions. 54 | 55 | #### `good first issue` & `intermediate quest` & `hard quest` 56 | Bugs and features that need to be worked on are also called "quest", which is a name inspired by many RPG games. Those represent the approximate difficulty and complexity of the given issue. 57 | 58 | ## Releasing and Deploying 59 | 60 | For each numbered version release, it is important to follow semantic versioning guidelines. 61 | - Bump version in `PermissionsSwiftUI.podspec` 62 | - Update changelog with changes (ignore for now) 63 | - Commit and push changes 64 | - Tag new version: 65 | ``` 66 | $ git tag -a -m "" 67 | $ git push origin --tags 68 | ``` 69 | - Deploy to Cocoapods: 70 | ``` 71 | pod trunk push --allow-warnings 72 | ``` 73 | The documentation will be automatically generated (Jazzy) and deployed to Github pages by a Github Action that triggers on each new release. 74 | 75 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/Mocks/MockHealthManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockHealthManager.swift.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/25/21. 6 | // 7 | 8 | import Foundation 9 | #if !os(tvOS) && PERMISSIONSWIFTUI_HEALTH 10 | import HealthKit 11 | protocol HealthManager { 12 | func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus 13 | func requestAuthorization(toShare typesToShare: Set?, read typesToRead: Set?, completion: @escaping (Bool, Error?) -> Void) 14 | static func isHealthDataAvailable() -> Bool 15 | 16 | } 17 | extension HKHealthStore: HealthManager {} 18 | 19 | final class MockHealthManager: HealthManager { 20 | enum AuthorizationStatus: CaseIterable { 21 | case notDetermined, authorized, denied, mixedAuthorized, mixedDenied 22 | } 23 | var authStatusOverride: AuthorizationStatus = .notDetermined 24 | var requestSuccessOverride: Bool = true 25 | static var healthDataAvailableOverride: Bool = true 26 | 27 | var requestedPermissions: HKAccess? 28 | var lastStatus: HKAuthorizationStatus = .notDetermined 29 | func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus { 30 | switch authStatusOverride { 31 | case .authorized: 32 | return .sharingAuthorized 33 | case .notDetermined: 34 | return .notDetermined 35 | case .denied: 36 | return .sharingDenied 37 | case .mixedAuthorized: 38 | return getCorrectPermission() 39 | case .mixedDenied: 40 | return getCorrectPermission() 41 | } 42 | 43 | } 44 | 45 | func getCorrectPermission() -> HKAuthorizationStatus { 46 | var keyStatus: HKAuthorizationStatus = .sharingAuthorized 47 | if self.authStatusOverride == .mixedDenied { 48 | keyStatus = .sharingDenied 49 | } 50 | if lastStatus == .notDetermined { 51 | self.lastStatus = keyStatus 52 | return keyStatus 53 | } 54 | else { 55 | self.lastStatus = .notDetermined 56 | return .notDetermined 57 | } 58 | } 59 | func requestAuthorization(toShare typesToShare: Set?, read typesToRead: Set?, completion: @escaping (Bool, Error?) -> Void) { 60 | let healthDataAvailable = MockHealthManager.healthDataAvailableOverride 61 | if requestSuccessOverride { 62 | completion(requestSuccessOverride, healthDataAvailable ? nil : fatalError("MockHealthManager - health data is not available")) 63 | guard typesToShare != nil || typesToRead != nil else {return} 64 | if let typesToShare = typesToShare, let typesToRead = typesToRead { 65 | requestedPermissions = .init(read: typesToRead as! Set, write: typesToShare) 66 | } 67 | else if let typesToShare = typesToShare { 68 | requestedPermissions = .init(write: typesToShare) 69 | } 70 | else if let typesToRead = typesToRead { 71 | requestedPermissions = .init(read: typesToRead as! Set) 72 | } 73 | 74 | } 75 | else { 76 | completion(false, NSError(domain: "", code: 0, userInfo: nil)) 77 | } 78 | } 79 | 80 | static func isHealthDataAvailable() -> Bool { 81 | healthDataAvailableOverride 82 | } 83 | 84 | 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIBiometrics/JMBiometricsPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMBiometricsPermissionManager.swift 3 | // 4 | // 5 | // Created by Nevio Hirani on 20.01.24. 6 | // Github: N3v1 - 7 | // 8 | 9 | import UIKit 10 | 11 | import LocalAuthentication 12 | import CorePermissionsSwiftUI 13 | 14 | /// A permission manager for handling biometric authentication requests. 15 | @available(iOS 13.0, macOS 11.0, *) 16 | public extension PermissionManager { 17 | /// Shared instance for managing biometric permissions. 18 | static let opticBiometrics = JMBiometricPermissionManager() 19 | } 20 | 21 | /// A permission manager specifically designed for handling biometric authentication requests, such as Face ID, Touch ID and Optic ID. 22 | /// 23 | /// `JMBiometricPermissionManager` provides a streamlined interface for checking and requesting biometric authentication permissions. 24 | /// It encapsulates the complexities associated with the LocalAuthentication framework, making it easy to integrate biometric security 25 | /// features into your app. The class is part of the `CorePermissionsSwiftUI` framework and aligns with the standardized `PermissionManager` protocol. 26 | /// 27 | /// ## Usage 28 | /// To utilize biometric authentication in your application, follow the guide in the README.md 29 | @available(iOS 13.0, macOS 11.0, *) 30 | public final class JMBiometricPermissionManager: PermissionManager { 31 | 32 | public override var permissionType: PermissionType { 33 | .biometrics 34 | } 35 | 36 | /// Retrieves the current authorization status for biometric authentication. 37 | /// 38 | /// - Returns: The current authorization status, indicating whether biometric authentication is authorized, denied, or not determined. 39 | public override var authorizationStatus: AuthorizationStatus { 40 | let context = LAContext() 41 | var error: NSError? 42 | 43 | if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { 44 | return .authorized 45 | } else { 46 | switch error?.code { 47 | case LAError.Code.biometryLockout.rawValue, LAError.Code.biometryNotAvailable.rawValue, 48 | LAError.Code.biometryNotEnrolled.rawValue: 49 | return .denied 50 | default: 51 | return .notDetermined 52 | } 53 | } 54 | } 55 | 56 | /// Requests permission for biometric authentication with a completion handler. 57 | /// 58 | /// - Parameters: 59 | /// - completion: A closure to be called once the request is processed. 60 | /// The closure takes a boolean indicating whether the permission was granted 61 | /// and an optional error in case of failure. 62 | public override func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 63 | let context = LAContext() 64 | 65 | let localizedReason = "Authenticate to access biometric features" 66 | 67 | context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: localizedReason) { success, error in 68 | DispatchQueue.main.async { 69 | if success { 70 | completion(true, nil) // Authorized (true), no error 71 | } else { 72 | completion(false, error) // Not authorized (false), with error 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Shared/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderText.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | struct HeaderView: View { 12 | @EnvironmentObject var store: PermissionStore 13 | @EnvironmentObject var schemaStore: PermissionSchemaStore 14 | var exitButtonAction:() -> Void 15 | //HeaderText component have slightly different UI for alert and modal. 16 | var mainText: MainTexts 17 | var body: some View { 18 | let style = schemaStore.permissionViewStyle 19 | VStack{ 20 | VStack{ 21 | if style == .alert{ 22 | Text("PERMISSIONS REQUEST") 23 | .font(.footnote) 24 | .fontWeight(.semibold) 25 | .foregroundColor(Color(.systemGray2)) 26 | //Make frame width take up as much space as possible, to make space for left align text 27 | .frame(maxWidth:.infinity, alignment: .leading) 28 | //Negative padding make the bottom title "shift up" a little 29 | .padding(.vertical, -5) 30 | .accessibility(identifier: "Alert header") 31 | } 32 | 33 | HStack { 34 | Text(.init(mainText.headerText)) 35 | .font(.system(style == .alert ? .title : .largeTitle, design: .rounded)) 36 | .fontWeight(.bold) 37 | .lineLimit(1) 38 | .minimumScaleFactor(0.85) 39 | .allowsTightening(true) 40 | .layoutPriority(1) 41 | .accessibility(identifier: "Main title") 42 | 43 | 44 | Spacer() 45 | ExitButtonSection(action: { 46 | exitButtonAction() 47 | guard let handler = store.configStore.onDisappearHandler else {return} 48 | handler(schemaStore.successfulPermissions, schemaStore.erroneousPermissions) 49 | }) 50 | //Layout priority does not do anything 51 | .layoutPriority(-1) 52 | } 53 | } 54 | //Extra padding for modal view only. Alert pop up space not enough for the extra paddings 55 | .padding(.top, style == .alert ? 0 : 30) 56 | .padding(.horizontal, style == .alert ? 0 : 16) 57 | 58 | if style == .modal { 59 | Text(.init(mainText.headerDescription)) 60 | .font(.system(.body, design: .rounded)) 61 | .fontWeight(.medium) 62 | .foregroundColor(Color(.systemGray)) 63 | .padding() 64 | //Limit line number to 3 for optimal UI, will automatically scale 65 | .lineLimit(3) 66 | //Can override parent's frame limits to allow adaptable font sizes on different device 67 | .fixedSize(horizontal: false, vertical: true) 68 | //Allow scacling down to half the original size on smaller screens 69 | .minimumScaleFactor(0.5) 70 | .textHorizontalAlign(.leading) 71 | .accessibility(identifier: "Modal header description") 72 | } 73 | 74 | } 75 | 76 | 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Model/PermissionType/PermissionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionModel.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/30/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | #if PERMISSIONSWIFTUI_HEALTH 11 | import HealthKit 12 | #endif 13 | 14 | /** 15 | The types of iOS system permission for show in the JMPermissions view 16 | 17 | Pass this as a parameter into JMPermissions View Modifier will display 3 UI elements–location, photo, and microphone. 18 | ``` 19 | [.location, photo, microphone] 20 | ``` 21 | */ 22 | @available(iOS 13.0, tvOS 13.0, *) 23 | public enum PermissionType: Hashable, Equatable { 24 | public static func == (lhs: PermissionType, rhs: PermissionType) -> Bool { 25 | lhs.rawValue == rhs.rawValue ? true : false 26 | } 27 | 28 | ///The `location` permission allows the device's positoin to be tracked 29 | case location 30 | 31 | ///Used to access the user's photo library 32 | case photo 33 | 34 | ///The `notification` permission allows the iOS system to receive notification from app 35 | case notification 36 | 37 | ///Permission that allows app to access device's bluetooth technologies 38 | case bluetooth 39 | 40 | ///Permission that allows Siri and Maps to communicate with your app 41 | case siri 42 | 43 | /// Permission that grants access to biometric authentication in your application. 44 | /// 45 | /// The `biometrics` permission enables the use of biometric authentication features, such as Face ID, Touch ID or OpticID, 46 | /// allowing users to securely authenticate themselves using their unique biometric data. 47 | @available(iOS 13, macOS 11, *) case biometrics 48 | 49 | ///In order for app to track user's data across apps and websites, the tracking permission is needed 50 | @available(iOS 14, tvOS 14, *) case tracking 51 | #if !os(tvOS) 52 | /** 53 | Permission that allows app to access healthkit information 54 | 55 | - Note: Extensive Info.plist values and configurations are required for HealthKit authorization. Please see Apple Developer [website](https://developer.apple.com/documentation/healthkit/authorizing_access_to_health_data) for details. \n 56 | 57 | For example, passing in a `Set` of `HKSampleType`: 58 | ``` 59 | [.health(categories: .init(readAndWrite: Set([HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!])))] 60 | ``` 61 | */ 62 | case health 63 | #endif 64 | 65 | ///The `locationAlways` permission provides location data even if app is in background 66 | @available(tvOS, unavailable) case locationAlways 67 | 68 | ///Permission allows developers to interact with the device microphone 69 | @available(tvOS, unavailable) case microphone 70 | 71 | ///Permission that allows developers to interact with on-device camera 72 | @available(tvOS, unavailable) case camera 73 | 74 | ///A permission that allows developers to read & write to device contacts 75 | @available(tvOS, unavailable) case contacts 76 | 77 | ///Permission that give app access to motion and fitness related sensor data 78 | @available(tvOS, unavailable) case motion 79 | 80 | ///Permission that allows app to read & write to device reminder before iOS 17 81 | @available(tvOS, unavailable, deprecated: 16.0, obsoleted: 17.0) 82 | case reminders 83 | 84 | ///Permission that allows app to read & write to device calendar before iOS 17 85 | @available(tvOS, unavailable, deprecated: 16.0, obsoleted: 17.0) 86 | case calendar 87 | 88 | ///Permission that allows app to use speech recognition 89 | @available(tvOS, unavailable) case speech 90 | 91 | ///Permission that allows app to control audio playback of the device 92 | @available(tvOS, unavailable) case music 93 | } 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/ConfigStore/AllButtonColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/18/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Customize the button colors 11 | /** 12 | `AllButtonColors` encapsulates the color configuration for all states of the allow button 13 | 14 | To customize button colors: 15 | 1. Define a new instance of the `AllButtonColors` struct 16 | 2. Add the `setAllowButtonColor(to colors:AllButtonColors)` modifier to your view 17 | 3. Pass in the `AllButtonColors` struct previously into the proper parameter 18 | */ 19 | @available(iOS 13.0, tvOS 13.0, *) 20 | public struct AllButtonColors: Equatable { 21 | var contentChanged: Bool { 22 | let allButtonColors = AllButtonColors() 23 | if self == allButtonColors {return false} 24 | return true 25 | } 26 | //MARK: Creating New Button Configs 27 | /** 28 | - parameters: 29 | - buttonIdle: The button color configuration for the default, idle state 30 | - buttonAllowed: The button color configuration for the highlighted, allowed state 31 | - buttonDenied: The button color configuration for the user explicitly denied state 32 | */ 33 | public init(buttonIdle: ButtonColor, buttonAllowed: ButtonColor, buttonDenied: ButtonColor){ 34 | self.init() 35 | self.buttonIdle = buttonIdle 36 | self.buttonAllowed = buttonAllowed 37 | self.buttonDenied = buttonDenied 38 | } 39 | /** 40 | - parameters: 41 | - buttonIdle: The button color configuration for the default, idle state 42 | */ 43 | public init(buttonIdle: ButtonColor){ 44 | self.init() 45 | self.buttonIdle = buttonIdle 46 | } 47 | /** 48 | - parameters: 49 | - buttonAllowed: The button color configuration for the highlighted, allowed state 50 | */ 51 | public init(buttonAllowed: ButtonColor){ 52 | self.init() 53 | self.buttonAllowed = buttonAllowed 54 | } 55 | /** 56 | - parameters: 57 | - buttonDenied: The button color configuration for the user explicitly denied state 58 | */ 59 | public init(buttonDenied: ButtonColor){ 60 | self.init() 61 | self.buttonDenied = buttonDenied 62 | } 63 | /** 64 | Initializes a new `AllbuttonColors` from the primary and tertiary colors 65 | 66 | Both `primaryColor` and `tertiaryColor` are non-required parameters. Colors without a given initializer parameter will be displayed with the default color. 67 | 68 | - parameters: 69 | - primaryColor: The primary color, characterized by the default blue 70 | - tertiaryColor: The tertiary color, characterized by the default alert red 71 | */ 72 | public init(primaryColor: Color?=nil, tertiaryColor: Color?=nil){ 73 | self.primaryColor = primaryColor ?? Color(.systemBlue) 74 | self.tertiaryColor = tertiaryColor ?? Color(.systemRed) 75 | self.buttonIdle = ButtonColor(foregroundColor: self.primaryColor, 76 | backgroundColor: Color(.systemGray5)) 77 | self.buttonAllowed = ButtonColor(foregroundColor: Color(.white), 78 | backgroundColor: self.primaryColor) 79 | self.buttonDenied = ButtonColor(foregroundColor: Color(.white), 80 | backgroundColor: self.tertiaryColor) 81 | } 82 | 83 | //MARK: Button Color States 84 | 85 | @usableFromInline var primaryColor: Color 86 | var tertiaryColor: Color 87 | ///The button color configuration under idle status defined by a `ButtonColor` struct 88 | public var buttonIdle: ButtonColor 89 | ///The button color configuration under allowed status defined by a `ButtonColor` struct 90 | public var buttonAllowed: ButtonColor 91 | ///The button color configuration under denied status defined by a `ButtonColor` struct 92 | public var buttonDenied: ButtonColor 93 | } 94 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIPhoto/JMPhotoPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMPhotoPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import CorePermissionsSwiftUI 11 | 12 | @available(iOS 13.0, tvOS 13.0, *) 13 | public extension PermissionManager { 14 | ///Used to access the user's photo library before iOS 14 15 | @available(iOS, deprecated, obsoleted: 100000, message: "Use 'authorizationStatus(for: )' instead after iOS 14.0.") 16 | static let photo = JMPhotoPermissionManager(requestedAccessLevel: .legacy) 17 | 18 | ///Used to read-only access the user's photo library 19 | @available(iOS 14.0, *) 20 | static let photoRead = JMPhotoPermissionManager(requestedAccessLevel: .writeOnly) 21 | 22 | ///Used to read and write to the user's photo library 23 | @available(iOS 14.0, *) 24 | static let photoFull = JMPhotoPermissionManager(requestedAccessLevel: .full) 25 | } 26 | 27 | @available(iOS 13.0, tvOS 13.0, *) 28 | public final class JMPhotoPermissionManager: PermissionManager { 29 | public init(requestedAccessLevel: AccessLevel = .legacy) { 30 | self.requestedAccessLevel = requestedAccessLevel 31 | if requestedAccessLevel == .legacy { 32 | NSLog("[PermissionsSwiftUI]: WARNING! Using legacy calendar or reminder permission, which will NOT work in iOS 17 and always return denied due to Apple EventKit API changes. Learn more: https://developer.apple.com/documentation/eventkit/accessing_the_event_store") 33 | } 34 | } 35 | public var requestedAccessLevel: AccessLevel 36 | 37 | public enum AccessLevel { 38 | case legacy 39 | @available(iOS 14, *) case writeOnly 40 | @available(iOS 14, *) case full 41 | } 42 | 43 | public override var permissionType: PermissionType { 44 | .photo 45 | } 46 | 47 | public override var authorizationStatus: AuthorizationStatus { 48 | var result: PHAuthorizationStatus? = nil 49 | switch requestedAccessLevel { 50 | case .legacy: 51 | result = PHPhotoLibrary.authorizationStatus() 52 | case .full: 53 | if #available(iOS 14, *) { 54 | result = PHPhotoLibrary.authorizationStatus(for: .readWrite) 55 | } 56 | case .writeOnly: 57 | if #available(iOS 14, *) { 58 | result = PHPhotoLibrary.authorizationStatus(for: .addOnly) 59 | } 60 | } 61 | switch result { 62 | case .authorized: 63 | return .authorized 64 | case .notDetermined: 65 | return .notDetermined 66 | case .limited: 67 | return .limited 68 | default: 69 | return .denied 70 | } 71 | } 72 | 73 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 74 | switch requestedAccessLevel { 75 | case .legacy: 76 | PHPhotoLibrary.requestAuthorization { status in 77 | self.handleAuthorizationResult(status, completion: completion) 78 | } 79 | case .full: 80 | if #available(iOS 14, *) { 81 | PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in 82 | self.handleAuthorizationResult(status, completion: completion) 83 | } 84 | } 85 | case .writeOnly: 86 | if #available(iOS 14, *) { 87 | PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in 88 | self.handleAuthorizationResult(status, completion: completion) 89 | } 90 | } 91 | default: 92 | completion(false, nil) 93 | } 94 | } 95 | 96 | private func handleAuthorizationResult(_ status: PHAuthorizationStatus, completion: @escaping (Bool, Error?) -> Void) { 97 | switch status { 98 | case .authorized, .limited: 99 | completion(true, nil) 100 | default: 101 | completion(false, nil) 102 | } 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Modifiers/Internal/ViewModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifier.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/2/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, tvOS 13.0, *) 11 | extension View { 12 | func buttonStatusColor(for allowButtonStatus: AllowButtonStatus) -> some View { 13 | self.modifier(ButtonStatusColor(allowButtonStatus: allowButtonStatus)) 14 | } 15 | func allowButton(foregroundColor: Color, backgroundColor: Color) -> some View { 16 | self.modifier(AllowButton(foregroundColor: foregroundColor, backgroundColor: backgroundColor)) 17 | } 18 | func alertViewFrame() -> some View { 19 | self.modifier(JMAlertViewFrame()) 20 | } 21 | func textHorizontalAlign(_ alignment: Alignment) -> some View { 22 | TextHorizontalAlign(alignment: alignment, bodyView: self) 23 | } 24 | 25 | @ViewBuilder 26 | func compatibleForegroundStyle(_ style: any ShapeStyle) -> some View { 27 | if #available(iOS 15, *) { 28 | self.foregroundStyle(style).typeErased() 29 | } 30 | else { 31 | self.foregroundColor(style as? Color).typeErased() 32 | } 33 | } 34 | } 35 | 36 | //Custom view modifier for the button component 37 | @available(iOS 13.0, tvOS 13.0, *) 38 | struct ButtonStatusColor: ViewModifier { 39 | var allowButtonStatus: AllowButtonStatus 40 | @EnvironmentObject var store: PermissionStore 41 | 42 | func body(content: Content) -> some View { 43 | let colorStore = { 44 | store.allButtonColors.contentChanged ? store.allButtonColors : store.configStore.allButtonColors 45 | }() 46 | switch self.allowButtonStatus { 47 | case .idle: 48 | return content.allowButton(foregroundColor: colorStore.buttonIdle.foregroundColor, 49 | backgroundColor: colorStore.buttonIdle.backgroundColor) 50 | 51 | case .allowed: 52 | return content.allowButton(foregroundColor: colorStore.buttonAllowed.foregroundColor, 53 | backgroundColor: colorStore.buttonAllowed.backgroundColor) 54 | 55 | 56 | case .denied: 57 | return content.allowButton(foregroundColor: colorStore.buttonDenied.foregroundColor, 58 | backgroundColor: colorStore.buttonDenied.backgroundColor) 59 | } 60 | } 61 | } 62 | //Custom modifier that nests within ButtonStatusColor to further extract code 63 | @available(iOS 13.0, tvOS 13.0, *) 64 | struct AllowButton: ViewModifier { 65 | var foregroundColor: Color 66 | var backgroundColor: Color 67 | var buttonSizeConstant :CGFloat { 68 | return screenSize.width < 400 ? 70-(1000-screenSize.width)/30 : 70 69 | } 70 | func body(content: Content) -> some View { 71 | content 72 | .frame(width: buttonSizeConstant) 73 | .font(.system(size: 15)) 74 | .minimumScaleFactor(0.2) 75 | .lineLimit(1) 76 | .foregroundColor(foregroundColor) 77 | .padding(.vertical,6) 78 | .padding(.horizontal, 6) 79 | .background( 80 | Capsule() 81 | .fill(backgroundColor) 82 | ) 83 | } 84 | } 85 | 86 | @available(iOS 13.0, tvOS 13.0, *) 87 | struct JMAlertViewFrame: ViewModifier { 88 | func body(content: Content) -> some View { 89 | content 90 | .background(Color(.systemBackground).opacity(0.8)) 91 | .frame(width: screenSize.width > 375 ? 375 : screenSize.width-60) 92 | .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 93 | .edgesIgnoringSafeArea(.all) 94 | } 95 | } 96 | @available(iOS 13.0, tvOS 13.0, *) 97 | struct TextHorizontalAlign: View { 98 | var alignment: Alignment 99 | var bodyView: BodyView 100 | @ViewBuilder 101 | var body: some View { 102 | switch alignment { 103 | case .leading: 104 | HStack{bodyView; Spacer()} 105 | case .trailing: 106 | HStack{Spacer(); bodyView} 107 | default: 108 | bodyView 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Modal-style/ModalViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMPermissionsView.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/30/21. 6 | // 7 | 8 | import SwiftUI 9 | import Introspect 10 | 11 | @available(iOS 13.0, tvOS 13.0, *) 12 | @usableFromInline struct ModalViewWrapper: View, CustomizableView { 13 | //store contains static configurations and customizations 14 | @usableFromInline var store: PermissionStore 15 | //schemaStore contains dynamically computed properties, and internal methods/properties 16 | @usableFromInline var schemaStore: PermissionSchemaStore 17 | //Keep track of whether modal as already been shown for 1st time 18 | @State var isModalNotShown = true 19 | @usableFromInline var showing: Binding 20 | @usableFromInline var bodyView: Body 21 | //Placeholder to make sure permissionsToAsk only get computed value once 22 | //Otherwise, the list of permissions will change while the modal is still open, which is not good 23 | var _permissionsToAsk: [PermissionManager]? 24 | var permissionsToAsk: [PermissionManager] { 25 | guard _permissionsToAsk == nil else { 26 | return _permissionsToAsk! 27 | } 28 | return schemaStore.undeterminedPermissions 29 | } 30 | var shouldShowPermission: Binding{ 31 | Binding(get: { 32 | //configStore.autoCheckAuth is added in newer version. autoCheckModalAuth is backward compatibility 33 | if (store.configStore.autoCheckAuth || 34 | (store.autoCheckModalAuth || store.autoCheckAlertAuth)) && 35 | //Prevent modal from unwanted dismiss while still presented 36 | isModalNotShown { 37 | //underterminedPermissions.isEmpty => No askable permissions => should not show modal 38 | return !permissionsToAsk.isEmpty 39 | } 40 | //Always show the modal regardless of permission status 41 | return true 42 | }, set: {_ in}) 43 | } 44 | @usableFromInline init(for bodyView: Body, 45 | showing: Binding, 46 | store: PermissionStore, 47 | permissionsToAsk: [PermissionManager]?=nil) { 48 | self.bodyView = bodyView 49 | self.showing = showing 50 | self._permissionsToAsk = permissionsToAsk 51 | self.store = store 52 | self.schemaStore = PermissionSchemaStore(store: store, 53 | permissionViewStyle: .modal) 54 | } 55 | 56 | @usableFromInline var body: some View { 57 | Group { 58 | bodyView 59 | .sheet(isPresented: showing.combine(with: shouldShowPermission), content: { 60 | ModalView(showModal: showing) 61 | //Possible nil, to account for backward compatibility 62 | .onAppear(perform: store.onAppear ?? store.configStore.onAppear) 63 | .onDisappear(perform: store.onDisappear ?? store.configStore.onDisappear) 64 | //Writing duplicate onAppear and OnDisappear is actually less lines of code 65 | .onAppear{isModalNotShown=false} 66 | .onDisappear{showing.wrappedValue = false; isModalNotShown=true} 67 | }) 68 | } 69 | .withEnvironmentObjects(store: store, permissionStyle: .modal) 70 | 71 | 72 | 73 | } 74 | //if DEBUG to ensure these functions are never used in production. They are for unit testing only. 75 | #if DEBUG 76 | func testCallOnAppear(){ 77 | guard let onAppear = store.configStore.onAppear else {return} 78 | onAppear() 79 | } 80 | func testCallOnDisappear(){ 81 | guard let onDisappear = store.configStore.onDisappear else {return} 82 | onDisappear() 83 | } 84 | #endif 85 | 86 | } 87 | //Extension Binding wrapper for Binding booleans 88 | @available(iOS 13.0, tvOS 13.0, *) 89 | extension Binding where Value == Bool{ 90 | func combine(with value2: Binding) -> Binding{ 91 | //Combine two Binding Bool conditions with AND operator 92 | return self.wrappedValue && value2.wrappedValue ? .constant(true) : .constant(false) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /docs/New_Permission_Guide.md: -------------------------------------------------------------------------------- 1 | # Contributor's Guide - Adding New Permissions 2 | > PermissionsSwiftUI offers incredible features, allowing developers to beautifully display and handle permissions in SwiftUI, in a easy-to-use yet unimaginable powerful way. Currently, PermissionsSwiftUI offers support for all 16 iOS system permissions, even the newest iOS 14.5 Tracking permission for developers to stay on the edge of Apple developer technologies. However, as Apple announces new software and operating systems over time, PermissionsSwiftUI will always have to keep up with the newest APIs which means possibly new system permissions that needs to be implemented. 3 | 4 | **The purpose of this guide is to offer a step-by-step, detailed instruction for contributors and the open source community on the process of adding a new permission support to PermissionsSwiftUI**. 5 | 6 | ## Prerequisites 7 | This guide assumes you have a supported version of Xcode installed. This guide assumes you understand how to fork the project, git clone the repository, as well as other basic git operations. This guide assumes you have a basic knowledge of the file structure of Swift Package projects. 8 | 9 | However, ONLY BEGINNER LEVEL Swift programming experience is required. 10 | 11 | ## Step 1 12 | Here I will use adding the Siri permission as a demonstration example. Let's imagine you just came across this amazing PermissionsSwiftUI library that will save you countless hours of coding work and presents your users a beautiful interface. You already have everything installed, and you begin to add a `JMModal` or `JMAlert` as shown in the quick start to show permission requests. But suddenly you realize the sad reality that PermissionsSwiftUI don't support the Siri permission, an critical part of your app. And instead of reaching out for the maintainer and creator, Jevon, for help, you decides to contribute to PermissionsSwiftUI and implement Siri permission. 13 | 14 | First, add a new `.target` to the Package.swift file's `permissionsTargets` to configure SPM to recognize a new submodule for Siri permission. 15 | 16 | image 17 | 18 | ## Step 2 19 | Create a new folder under the Sources folder, and add new file named "JM\(Permission name)PermissionManager.swift", in this case "JMSiriPermissionManager.swift". 20 | 21 | image 22 | 23 | ## Step 3 24 | Head over to the PermissionType.swift file, where you will find an enum called `PermissionType`. Scroll to bottom of page, add a new enum type for the permission. Don't forget to add documentation with /// slashes, and mark availability as needed. 25 | 26 | image 27 | 28 | ## Step 4 29 | Google search for Apple documentation on the particular permission. You will need to locate the relevant framework, classes, and most importantly a function related to `AuthorizationStatus` and a function related to `requestAuthorization`. 30 | Use the previously mentioned 2 functions to create a JMPermissionManager class that inherits from `PermissionManager` and provide concrete implementation for some of the parent's methods and properties. 31 | 32 | In addition, provide an extension to `PermissionManager` to define a short name for the permission type like siri, to allow for enum like syntax access in the API. 33 | 34 | Use the following example as template: 35 | 36 | ```swift 37 | @available(iOS 13.0, tvOS 13.0, *) 38 | public extension PermissionManager { 39 | static let siri = JMSiriPermissionManager() 40 | } 41 | 42 | @available(iOS 13.0, tvOS 13.0, *) 43 | public final JMSiriPermissionManager: PermissionManager { 44 | 45 | 46 | public override var permissionType: PermissionType { 47 | .siri 48 | } 49 | 50 | public override var authorizationStatus: AuthorizationStatus { 51 | switch INPreferences.siriAuthorizationStatus() { 52 | case .authorized: 53 | return .authorized 54 | case .notDetermined: 55 | return .notDetermined 56 | default: 57 | return .denied 58 | } 59 | } 60 | 61 | public override func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 62 | INPreferences.requestSiriAuthorization {authorizationStatus in 63 | if authorizationStatus == .authorized { 64 | completion(true, nil) //Authorized (true), no error 65 | } 66 | else { 67 | completion(false, nil) //Not authorized (false), no error 68 | } 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | ## Step 5 75 | If you compile the library at this point, you will probably get errors from the PermissionComponentsStore file. Head over to the error source, and first add a new variable for the new permission to `PermissionComponentsStore`. The variable should be a `JMPermission` and now you get to choose a nice SFSymbol image, a title, and a description for this permission! 76 | 77 | Then, scroll down in same file to an extension that contains function `getPermissionComponent`. Following the context code conventions and using existing code as template, implement the switch case for the new permission. 78 | 79 | ## Step 6 80 | Just kidding, there is no step 6 😂! That's all there is to adding a brand new permission support to PermissionsSwiftUI library. Very simple and intuitive, right? Your contribution to PermissionsSwiftUI will be made available to thousands of developers using this library worldwide, I can't wait to see what permissions you implement in pull requests. 81 | -------------------------------------------------------------------------------- /Sources/PermissionsSwiftUIHealth/JMHealthPermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JMHealthPermissionManager.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 2/10/21. 6 | // 7 | 8 | import Foundation 9 | #if !os(tvOS) && PERMISSIONSWIFTUI_HEALTH 10 | import CorePermissionsSwiftUI 11 | import HealthKit 12 | 13 | @available(iOS 13.0, tvOS 13.0, *) 14 | public extension PermissionManager { 15 | /** 16 | Permission that allows app to access healthkit information 17 | 18 | - Note: Extensive Info.plist values and configurations are required for HealthKit authorization. Please see Apple Developer [website](https://developer.apple.com/documentation/healthkit/authorizing_access_to_health_data) for details. \n 19 | 20 | For example, passing in a `Set` of `HKSampleType`: 21 | ``` 22 | [.health(categories: .init(readAndWrite: Set([HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!])))] 23 | ``` 24 | 25 | - Attention: From Apple Developer Documentation: "to help prevent possible leaks of sensitive health information, your app cannot determine whether or not a user has granted permission to read data. If you are not given permission, it simply appears as if there is no data of the requested type in the HealthKit store." 26 | */ 27 | static func health(categories: HKAccess) -> JMHealthPermissionManager { 28 | JMHealthPermissionManager(categories: categories) 29 | } 30 | } 31 | 32 | @available(iOS 13.0, tvOS 13.0, *) 33 | public class JMHealthPermissionManager: PermissionManager { 34 | 35 | typealias authorizationStatus = HKAuthorizationStatus 36 | typealias CountComparison = (Int, Int) 37 | var categories: HKAccess 38 | let healthStore = HKHealthStore() 39 | override public var permissionType: PermissionType { 40 | get { 41 | .health 42 | } 43 | } 44 | 45 | init(categories: HKAccess) { 46 | self.categories = categories 47 | } 48 | 49 | /** 50 | - Note: From Apple Developer Documentation: "to help prevent possible leaks of sensitive health information, your app cannot determine whether or not a user has granted permission to read data. If you are not given permission, it simply appears as if there is no data of the requested type in the HealthKit store." 51 | */ 52 | public override var authorizationStatus: AuthorizationStatus { 53 | get { 54 | var allowDenyCount: CountComparison = (authorized: 0, denied: 0) //Tracks # of authorized and denied health categories 55 | var status: AuthorizationStatus { 56 | 57 | //Set to notDetermined if all permissions are not determined 58 | if allowDenyCount.0 == 0 && allowDenyCount.1 == 0 { 59 | return .notDetermined 60 | } 61 | 62 | //Set to authorized if at least 1 type is authorized 63 | if allowDenyCount.0 > 0 { 64 | return .authorized 65 | } 66 | 67 | //If all types are denied, set status to denied 68 | else { 69 | return .denied 70 | } 71 | } 72 | 73 | //Map the authorization status, remove allowed and denied permissions from array. 74 | //Increase allowDenyCount as needed. 75 | mapPermissionAuthorizationStatus(for: categories.writePermissions, forCount: &allowDenyCount) 76 | 77 | //Assume all read permissions are authorized, because Apple restrict app from determining read data 78 | if categories.writePermissions.isEmpty { 79 | allowDenyCount.0 += categories.readPermissions.count 80 | } 81 | return status 82 | } 83 | 84 | } 85 | func mapPermissionAuthorizationStatus(for permissions: Set, 86 | forCount allowDenyCount: inout CountComparison) { 87 | for sampleType in permissions { 88 | switch healthStore.authorizationStatus(for: sampleType){ 89 | case .sharingAuthorized: 90 | allowDenyCount.0 += 1 91 | case .sharingDenied: 92 | allowDenyCount.1 += 1 93 | default: 94 | () 95 | } 96 | } 97 | } 98 | override public func requestPermission(completion: @escaping (Bool, Error?) -> Void) { 99 | guard HKHealthStore.isHealthDataAvailable() else { 100 | #if DEBUG 101 | print("PermissionsSwiftUI - Health data is not available") 102 | #endif 103 | completion(false, createUnavailableError()) 104 | return 105 | } 106 | healthStore.requestAuthorization(toShare: Set(categories.writePermissions), 107 | read: Set(categories.readPermissions)) { authorized, error in 108 | completion(self.authorizationStatus == .authorized, error) 109 | } 110 | 111 | } 112 | func createUnavailableError() -> NSError { 113 | let userInfo: [String: Any] = [ 114 | NSLocalizedDescriptionKey: 115 | NSLocalizedString("Health permission request couldn't be completed.", 116 | comment: "localizedErrorDescription"), 117 | NSLocalizedFailureReasonErrorKey: 118 | NSLocalizedString("Health data is not available on the current device, the permission cannot be requested.", 119 | comment: "localizedErrorFailureReason"), 120 | NSLocalizedRecoverySuggestionErrorKey: 121 | NSLocalizedString("Verify that HealthKit is available on the current device.", 122 | comment: "localizedErrorRecoverSuggestion") 123 | ] 124 | return NSError(domain: "com.jevonmao.permissionsswiftui", code: 1, userInfo: userInfo) 125 | } 126 | } 127 | #endif 128 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/SwiftUI/Shared/PermissionSectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionSectionCell.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 6/11/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum AllowButtonStatus: CaseIterable { 11 | case idle 12 | case allowed 13 | case denied 14 | } 15 | 16 | @available(iOS 13.0, tvOS 13.0, *) 17 | struct PermissionSectionCell: View { 18 | @State var permissionManager: PermissionManager 19 | @State var allowButtonStatus: AllowButtonStatus = .idle 20 | @Binding var showing: Bool 21 | @EnvironmentObject var store: PermissionStore 22 | @EnvironmentObject var schemaStore: PermissionSchemaStore 23 | 24 | //Empty unauthorized array means all permissions have been interacted 25 | var shouldAutoDismiss: Bool {FilterPermissions.filterForUnauthorized(with: store.permissions, store: schemaStore).isEmpty} 26 | 27 | //Computed constants based on device size for dynamic UI 28 | var screenSizeConstant: CGFloat { 29 | //Weirdass formulas that simply work 30 | screenSize.width < 400 ? 40-(1000-screenSize.width)/80 : 40 31 | } 32 | var fontSizeConstant: CGFloat { 33 | screenSize.width < 400 ? 20-(1000-screenSize.width)/150 : 20 34 | } 35 | var smallFontSizeConstant: CGFloat { 36 | fontSizeConstant - fontSizeConstant/2.8 37 | } 38 | var vertPaddingConstant: CGFloat { 39 | if schemaStore.permissionViewStyle == .alert { 40 | return screenSize.width < 400 ? 0 : 10 41 | } 42 | else{ 43 | return 15 44 | } 45 | } 46 | var horiPaddingConstant: CGFloat { 47 | if schemaStore.permissionViewStyle == .alert { 48 | return 0 49 | } 50 | else{ 51 | return 10 52 | } 53 | } 54 | var body: some View { 55 | let currentPermission = schemaStore.permissionComponentsStore.getPermissionComponent(for: permissionManager.permissionType) 56 | HStack { 57 | currentPermission.imageIcon 58 | .foregroundColor(store.configStore.allButtonColors.primaryColor) 59 | .font(.system(size: screenSizeConstant)) 60 | .frame(width: screenSizeConstant) 61 | .padding(.horizontal, 5) 62 | .accessibility(identifier: "Permission icon") 63 | 64 | VStack(alignment: .leading) { 65 | Text(currentPermission.title) 66 | .font(.system(size: fontSizeConstant)) 67 | .bold() 68 | .lineLimit(1) 69 | .minimumScaleFactor(0.7) 70 | .layoutPriority(1) 71 | .accessibility(identifier: "Permission title") 72 | 73 | Text(currentPermission.description) 74 | .font(.system(size: smallFontSizeConstant)) 75 | .lineLimit(3) 76 | .compatibleForegroundStyle(store.configStore.permissionDescriptionForeground) 77 | .minimumScaleFactor(0.5) 78 | .accessibility(identifier: "Permission description") 79 | 80 | } 81 | .padding(.horizontal, 3) 82 | Spacer() 83 | 84 | let useAltText = store.configStore.mainTexts.useAltButtonLabel 85 | if schemaStore.permissionViewStyle == .alert { 86 | //No animation for alert to avoid unwanted jiggle 87 | AllowButtonSection(action: handlePermissionRequest, 88 | useAltText: useAltText, 89 | allowButtonStatus: $allowButtonStatus) 90 | } 91 | else{ 92 | //Separate case for modal style because an animation is needed for best user experience 93 | AllowButtonSection(action: handlePermissionRequest, 94 | useAltText: useAltText, 95 | allowButtonStatus: $allowButtonStatus) 96 | } 97 | 98 | } 99 | .fixedSize(horizontal: false, vertical: true) 100 | .padding(.vertical, vertPaddingConstant) 101 | .padding(.horizontal, horiPaddingConstant) 102 | } 103 | 104 | func handlePermissionRequest() { 105 | permissionManager.requestPermission{authorized, error in 106 | DispatchQueue.main.async { 107 | let result = JMResult(permissionType: permissionManager.permissionType, 108 | authorizationStatus: permissionManager.authorizationStatus, 109 | error: error) 110 | 111 | updateSchemaStore(authorized: authorized, result: result) 112 | schemaStore.objectWillChange.send() 113 | handleCompletionDismissal() 114 | 115 | } 116 | } 117 | } 118 | 119 | func updateSchemaStore(authorized: Bool, result: JMResult) { 120 | schemaStore.permissionComponentsStore.getPermissionComponent(for: permissionManager.permissionType) {permissionComponent in 121 | permissionComponent.interacted = true 122 | if authorized { 123 | allowButtonStatus = .allowed 124 | permissionComponent.authorized = true 125 | (schemaStore.successfulPermissions?.append(result)) ?? (schemaStore.successfulPermissions = [result]) 126 | } 127 | else { 128 | allowButtonStatus = .denied 129 | permissionComponent.authorized = false 130 | (schemaStore.erroneousPermissions?.append(result)) ?? (schemaStore.erroneousPermissions = [result]) 131 | } 132 | } 133 | } 134 | 135 | func handleCompletionDismissal() { 136 | //Backward compatibility - autoDismissAlert, autoDismissModal, and autoDismiss are all acceptable ways to trigger condition 137 | if shouldAutoDismiss && 138 | 139 | //Current view style is alert and autoDismissAlert is true 140 | ((schemaStore.permissionViewStyle == .alert && 141 | store.autoDismissAlert) || 142 | //Current view style is modal and autoDismissModal is true 143 | (schemaStore.permissionViewStyle == .modal && 144 | store.autoDismissModal)) && 145 | store.configStore.autoDismiss { 146 | DispatchQueue.main.asyncAfter(deadline: .now()+0.8) { 147 | showing = false 148 | guard let handler = store.configStore.onDisappearHandler else {return} 149 | handler(schemaStore.successfulPermissions ?? nil, schemaStore.erroneousPermissions ?? nil) 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Modifiers/Public/PermissionCustomModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionCustomizeModifiers.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 1/31/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | //MARK: Customize Permission Components 11 | @available(iOS 13.0, tvOS 13.0, *) 12 | public extension CustomizableView { 13 | /** 14 | Customizes the image view, title (optional), and description (optional) of any permission component 15 | 16 | Use this modifier on your existing view to set all the available customizations–the symbol image , and title (optional) and description (optional). 17 | 18 | For example, 19 | ``` 20 | YourTopLevelView() 21 | .setPermissionComponents(for: .camera, 22 | image: AnyView(Image(systemName: "camera.fill")), 23 | title: "Camera", 24 | description: "Camera description") 25 | ``` 26 | will override the default image, title , and description for the `camera` permission. It will result in a camera image symbol, and the custom title and description 27 | - Parameters: 28 | - for: `PermissonType` specifying the permission component 29 | - image: The color to render the symbol image 30 | - title: The title text (optional) 31 | - description: The description text (optional) 32 | */ 33 | 34 | @inlinable func setPermissionComponent(for permission: PermissionType, image:AnyView, title: String?=nil, description: String?=nil) -> some CustomizableView { 35 | store.permissionComponentsStore.getPermissionComponent(for: permission){permissionComponent in 36 | permissionComponent.title = title ?? permissionComponent.title 37 | permissionComponent.imageIcon = image 38 | permissionComponent.description = description ?? permissionComponent.description 39 | } 40 | return self 41 | } 42 | 43 | /** 44 | Customizes only the title of any permission component 45 | 46 | Use this modifier on your existing view to set the title customization. 47 | 48 | For example, 49 | ``` 50 | YourTopLevelView() 51 | .setPermissionComponents(for: .camera, 52 | title: "Camera") 53 | ``` 54 | will override the default title for the `camera` permission. It will result in a custom title. 55 | - Parameters: 56 | - for: `PermissonType` specifying the permission component 57 | - title: The title text 58 | */ 59 | 60 | @inlinable func setPermissionComponent(for permission: PermissionType, title: String) -> some CustomizableView { 61 | store.permissionComponentsStore.getPermissionComponent(for: permission){permissionComponent in 62 | permissionComponent.title = title 63 | } 64 | return self 65 | } 66 | 67 | /** 68 | Customizes only the description of any permission component 69 | 70 | Use this modifier on your existing view to set the description customization. 71 | 72 | For example, 73 | ``` 74 | YourTopLevelView() 75 | .setPermissionComponents(for: .camera, 76 | description: "Camera description") 77 | ``` 78 | will override the default title for the `camera` permission. It will result in a custom description. 79 | - Parameters: 80 | - for: `PermissonType` specifying the permission component 81 | - description: The description text 82 | */ 83 | 84 | @inlinable func setPermissionComponent(for permission: PermissionType, description: String) -> some CustomizableView { 85 | store.permissionComponentsStore.getPermissionComponent(for: permission){$0.description = description} 86 | return self 87 | } 88 | } 89 | 90 | //MARK: Configure Allow Button Colors 91 | @available(iOS 13.0, tvOS 13.0, *) 92 | public extension CustomizableView { 93 | /** 94 | Customizes the color of allow buttons for all status states 95 | 96 | The customization of button colors with this modifier applies to both `JMAlert` and `JMModal` views 97 | 98 | To customize button colors: 99 | 1. Define a new instance of the `AllButtonColors` struct 100 | 2. Add the `setAllowButtonColor(to colors:AllButtonColors)` modifier to your view 101 | 3. Pass in the `AllButtonColors` struct previously into the proper parameter 102 | 103 | - Parameters: 104 | - for: `PermissonType` specifying the permission component 105 | - description: The description text 106 | */ 107 | 108 | @inlinable func setAllowButtonColor(to colors:AllButtonColors) -> some CustomizableView { 109 | store.configStore.allButtonColors = colors 110 | return self 111 | } 112 | } 113 | 114 | 115 | //MARK: Set Overall Accent Color 116 | @available(iOS 13.0, tvOS 13.0, *) 117 | public extension CustomizableView { 118 | /** 119 | Customizes the overall accent color of PermissionsSwiftUI views. 120 | 121 | The customization of accent color with this modifier applies to both `JMAlert` and `JMModal` views. The new accent color will replace the default Apple system blue color for image icons, as well as button foreground and background colors. 122 | 123 | - Parameters: 124 | - to: The new customized accent color 125 | */ 126 | 127 | @inlinable func setAccentColor(to color: Color) -> some CustomizableView { 128 | store.configStore.allButtonColors.primaryColor = color 129 | return self 130 | } 131 | 132 | /** 133 | Customizes the primary and tertiary color of PermissionsSwiftUI views. 134 | 135 | The customization of colors with this modifier applies to both `JMAlert` and `JMModal` views. 136 | * The new primary color will replace the default Apple system blue color for image icons, as well as button foreground and background colors. 137 | * The new tertiary color will replace the default Apple system red color for the `Denied` state of buttons. 138 | 139 | - Parameters: 140 | - toPrimary: The new customized primary color 141 | - toTertiary: The new customized tertiary color 142 | 143 | */ 144 | 145 | @inlinable func setAccentColor(toPrimary primaryColor: Color, toTertiary tertiaryColor: Color) -> some CustomizableView { 146 | let buttonColors = AllButtonColors(primaryColor: primaryColor, 147 | tertiaryColor: tertiaryColor) 148 | store.configStore.allButtonColors = buttonColors 149 | return self 150 | } 151 | 152 | /** 153 | Customizes the text foreground style for all the permission descriptions. 154 | 155 | - Parameters: 156 | - foreground: The new customized color 157 | 158 | */ 159 | 160 | @inlinable func setPermissionDescription(foreground color: any ShapeStyle) -> some CustomizableView { 161 | store.configStore.permissionDescriptionForeground = color 162 | return self 163 | } 164 | } 165 | 166 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let permissionsTargets: [Target] = [ 7 | .target( 8 | name: "CorePermissionsSwiftUI", //Internal module for shared code 9 | dependencies: ["Introspect"], 10 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"], 11 | resources: [.process("Resources")] 12 | ), 13 | .target( 14 | name: "PermissionsSwiftUI", //Maintain backward compatibility - access to all permissions 15 | dependencies: ["Introspect", "CorePermissionsSwiftUI", "PermissionsSwiftUITracking", "PermissionsSwiftUIBluetooth", "PermissionsSwiftUICalendar", "PermissionsSwiftUICamera", "PermissionsSwiftUIContacts", "PermissionsSwiftUILocation", "PermissionsSwiftUILocationAlways", "PermissionsSwiftUIMicrophone", "PermissionsSwiftUIMotion", "PermissionsSwiftUIMusic", "PermissionsSwiftUINotification", "PermissionsSwiftUIPhoto", "PermissionsSwiftUIReminder", "PermissionsSwiftUISpeech", "PermissionsSwiftUIHealth"], 16 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 17 | ), 18 | .target( 19 | name: "PermissionsSwiftUIBluetooth", 20 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 21 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 22 | ), 23 | .target( 24 | name: "PermissionsSwiftUICalendar", 25 | dependencies: ["Introspect", "CorePermissionsSwiftUI", "PermissionsSwiftUIEvent"], 26 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 27 | ), 28 | .target( 29 | name: "PermissionsSwiftUICamera", 30 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 31 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 32 | ), 33 | .target( 34 | name: "PermissionsSwiftUIContacts", 35 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 36 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 37 | ), 38 | .target( 39 | name: "PermissionsSwiftUIHealth", 40 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 41 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"], 42 | swiftSettings: [ 43 | .define("PERMISSIONSWIFTUI_HEALTH") 44 | ] 45 | ), 46 | .target( 47 | name: "PermissionsSwiftUILocationAlways", 48 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 49 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"], 50 | swiftSettings: [ 51 | .define("PERMISSIONSWIFTUI_LOCATION") 52 | ] 53 | ), 54 | .target( 55 | name: "PermissionsSwiftUILocation", 56 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 57 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"], 58 | swiftSettings: [ 59 | .define("PERMISSIONSWIFTUI_LOCATION") 60 | ] 61 | ), 62 | .target( 63 | name: "PermissionsSwiftUIMicrophone", 64 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 65 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 66 | ), 67 | .target( 68 | name: "PermissionsSwiftUIMotion", 69 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 70 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 71 | ), 72 | .target( 73 | name: "PermissionsSwiftUIMusic", 74 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 75 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 76 | ), 77 | .target( 78 | name: "PermissionsSwiftUINotification", 79 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 80 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"], 81 | swiftSettings: [ 82 | .define("PERMISSIONSWIFTUI_NOTIFICATION") 83 | ] 84 | ), 85 | .target( 86 | name: "PermissionsSwiftUIPhoto", 87 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 88 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"], 89 | swiftSettings: [ 90 | .define("PERMISSIONSWIFTUI_PHOTO") 91 | ] 92 | ), 93 | .target( 94 | name: "PermissionsSwiftUIReminder", 95 | dependencies: ["Introspect", "CorePermissionsSwiftUI", "PermissionsSwiftUIEvent"], 96 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 97 | ), 98 | .target( 99 | name: "PermissionsSwiftUISpeech", 100 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 101 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 102 | ), 103 | .target( 104 | name: "PermissionsSwiftUITracking", 105 | dependencies: ["Introspect", .target(name: "CorePermissionsSwiftUI")], 106 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 107 | ), 108 | .target( 109 | name: "PermissionsSwiftUISiri", 110 | dependencies: ["Introspect", "CorePermissionsSwiftUI"], 111 | exclude: ["../../Tests/PermissionsSwiftUITests/__Snapshots__"] 112 | ), 113 | .target( 114 | name: "PermissionsSwiftUIBiometrics", 115 | dependencies: ["Introspect", .target(name: "CorePermissionsSwiftUI")], 116 | exclude: ["../../Tsts/PermissionsSwiftUITests/__Snapshots__"] 117 | ), 118 | .target( 119 | name: "PermissionsSwiftUIEvent", 120 | dependencies: ["Introspect", .target(name: "CorePermissionsSwiftUI")], 121 | exclude: ["../../Tsts/PermissionsSwiftUITests/__Snapshots__"] 122 | )] 123 | 124 | let package = Package( 125 | name: "PermissionsSwiftUI", 126 | defaultLocalization: "en", 127 | platforms: [.iOS(.v13), .macOS(.v10_15)], 128 | products: permissionsTargets.map{Product.library(name: $0.name, targets: [$0.name])}, 129 | dependencies: [ 130 | // Dependencies declare other packages that this package depends on. 131 | .package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", "1.0.0"..<"2.0.0"), 132 | .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect", "0.0.0"..<"1.0.0") 133 | ], 134 | targets: [ 135 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 136 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 137 | .testTarget(name: "PermissionsSwiftUITests", 138 | dependencies: ["SnapshotTesting", "PermissionsSwiftUI", "CorePermissionsSwiftUI"], 139 | exclude: [], 140 | resources: [.process("__Snapshots__")]), 141 | .testTarget( 142 | name: "PermissionsSwiftUISmallScreenTests", 143 | dependencies: ["SnapshotTesting"] + permissionsTargets 144 | .map{Target.Dependency(stringLiteral: $0.name)}, 145 | exclude: [], 146 | resources: [.process("__Snapshots__")] 147 | ), 148 | 149 | ] + permissionsTargets 150 | ) 151 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/PermissionStore/BackwardCompatibilityExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/18/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /** 12 | Additional configuration properties for backward compatibility 13 | 14 | - Warning: These properties are deprecated, access the sub-properties through `PermissionStore` 's `configStore` property instead 15 | */ 16 | @available(iOS, introduced: 13.0, obsoleted: 15.0, deprecated, message: "Access the properties through PermissionStore's configStore property instead. Learn more: https://github.com/jevonmao/PermissionsSwiftUI/wiki/Migrating-to-v1.4.0") 17 | @available(tvOS, introduced: 13.0, obsoleted: 15.0, deprecated, message: "Access the properties through PermissionStore's configStore property instead. Learn more: https://github.com/jevonmao/PermissionsSwiftUI/wiki/Migrating-to-v1.4.0") 18 | public extension PermissionStore { 19 | 20 | //MARK: Configuring View Texts 21 | ///The text for text label components, including header and descriptions 22 | var mainTexts: MainTexts { 23 | get {configStore.mainTexts} 24 | set {configStore.mainTexts = newValue} 25 | } 26 | 27 | //MARK: Customizing Colors 28 | ///The color configuration for permission allow buttons 29 | var allButtonColors: AllButtonColors { 30 | get {configStore.allButtonColors} 31 | set {configStore.allButtonColors = newValue} 32 | } 33 | 34 | //MARK: Change Auto Dismiss Behaviors 35 | ///Whether to auto dismiss the modal after last permission is allowed 36 | var autoDismissModal: Bool { 37 | get {configStore.autoDismiss} 38 | set {configStore.autoDismiss = newValue} 39 | } 40 | 41 | ///Whether to auto dismiss the alert after last permission is allowed 42 | var autoDismissAlert: Bool { 43 | get {configStore.autoDismiss} 44 | set {configStore.autoDismiss = newValue} 45 | } 46 | 47 | //MARK: Configure Auto Authorization Checking 48 | ///Whether to auto check for authorization status before showing, and show the view only if permission is in `notDetermined` 49 | var autoCheckModalAuth: Bool { 50 | get {configStore.autoCheckAuth} 51 | set {configStore.autoCheckAuth = newValue} 52 | } 53 | 54 | ///Whether to auto check for authorization status before showing, and show the view only if permission is in `notDetermined` 55 | var autoCheckAlertAuth: Bool { 56 | get {configStore.autoCheckAuth} 57 | set {configStore.autoCheckAuth = newValue} 58 | } 59 | 60 | //MARK: Prevent Dismissal Before All Permissions Interacted 61 | ///Whether to prevent dismissal of modal view before all permissions have been interacted (explict deny or allow) 62 | var restrictModalDismissal: Bool { 63 | get {configStore.restrictDismissal} 64 | set {configStore.restrictDismissal = newValue} 65 | } 66 | ///Whether to prevent dismissal of alert view before all permissions have been interacted (explict deny or allow) 67 | var restrictAlertDismissal: Bool { 68 | get {configStore.restrictDismissal} 69 | set {configStore.restrictDismissal = newValue} 70 | } 71 | 72 | //MARK: `onAppear` and `onDisappear` Executions 73 | ///Override point for executing action when PermissionsSwiftUI view appears 74 | var onAppear: (()->Void)? { 75 | get {configStore.onAppear} 76 | set {configStore.onAppear = newValue} 77 | } 78 | ///Override point for executing action when PermissionsSwiftUI view disappears 79 | var onDisappear: (()->Void)? { 80 | get {configStore.onDisappear} 81 | set {configStore.onDisappear = newValue} 82 | } 83 | } 84 | 85 | @available(iOS, introduced: 13.0, obsoleted: 15.0, deprecated, message: "These will no longer work. Access through permissionComponentsStore property instead. Learn more: https://github.com/jevonmao/PermissionsSwiftUI/wiki/Migrating-to-v1.4.0") 86 | @available(tvOS, introduced: 13.0, obsoleted: 15.0, deprecated, message: "These will no longer work. Access through permissionComponentsStore property instead. Learn more: https://github.com/jevonmao/PermissionsSwiftUI/wiki/Migrating-to-v1.4.0") 87 | /** 88 | Additional permission component properties for backward compatibility 89 | 90 | - Warning: These properties are deprecated, access the sub-properties through `PermissionStore` 's `permissionComponentsStore` property instead 91 | */ 92 | extension PermissionStore { 93 | //MARK: Permission Components 94 | ///The displayed text and image icon for the camera permission 95 | public var cameraPermission: JMPermission { 96 | get {permissionComponentsStore.cameraPermission} 97 | set {permissionComponentsStore.cameraPermission = newValue} 98 | } 99 | ///The displayed text and image icon for the location permission 100 | public var locationPermission: JMPermission { 101 | get {permissionComponentsStore.locationPermission} 102 | set {permissionComponentsStore.locationPermission = newValue} 103 | } 104 | ///The displayed text and image icon for the location always permission 105 | public var locationAlwaysPermission: JMPermission { 106 | get {permissionComponentsStore.locationAlwaysPermission} 107 | set {permissionComponentsStore.locationAlwaysPermission = newValue} 108 | } 109 | ///The displayed text and image icon for the photo library permission 110 | public var photoPermission: JMPermission { 111 | get {permissionComponentsStore.photoPermission} 112 | set {permissionComponentsStore.photoPermission = newValue} 113 | } 114 | ///The displayed text and image icon for the microphone permission 115 | public var microphonePermisson: JMPermission { 116 | get {permissionComponentsStore.microphonePermisson} 117 | set {permissionComponentsStore.microphonePermisson = newValue} 118 | } 119 | ///The displayed text and image icon for the notification center permission 120 | public var notificationPermission: JMPermission { 121 | get {permissionComponentsStore.notificationPermission} 122 | set {permissionComponentsStore.notificationPermission = newValue} 123 | } 124 | ///The displayed text and image icon for the calendar permission 125 | public var calendarPermisson: JMPermission { 126 | get {permissionComponentsStore.calendarPermisson} 127 | set {permissionComponentsStore.calendarPermisson = newValue} 128 | } 129 | ///The displayed text and image icon for the bluetooth permission 130 | public var bluetoothPermission: JMPermission { 131 | get {permissionComponentsStore.bluetoothPermission} 132 | set {permissionComponentsStore.bluetoothPermission = newValue} 133 | } 134 | ///The displayed text and image icon for the permission to track across apps and websites 135 | public var trackingPermission : JMPermission { 136 | get {permissionComponentsStore.trackingPermission} 137 | set {permissionComponentsStore.trackingPermission = newValue} 138 | } 139 | ///The displayed text and image icon for the contact permission 140 | public var contactsPermission: JMPermission { 141 | get {permissionComponentsStore.contactsPermission} 142 | set {permissionComponentsStore.contactsPermission = newValue} 143 | } 144 | ///The displayed text and image icon for the motion permission 145 | public var motionPermission: JMPermission { 146 | get {permissionComponentsStore.motionPermission} 147 | set {permissionComponentsStore.motionPermission = newValue} 148 | } 149 | ///The displayed text and image icon for the reminders permission 150 | public var remindersPermission: JMPermission { 151 | get {permissionComponentsStore.remindersPermission} 152 | set {permissionComponentsStore.remindersPermission = newValue} 153 | } 154 | ///The displayed text and image icon for the speech recognition permission 155 | public var speechPermission: JMPermission { 156 | get {permissionComponentsStore.speechPermission} 157 | set {permissionComponentsStore.speechPermission = newValue} 158 | } 159 | ///The displayed text and image icon for the health permission 160 | public var healthPermission: JMPermission { 161 | get {permissionComponentsStore.healthPermission} 162 | set {permissionComponentsStore.healthPermission = newValue} 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/CorePermissionsSwiftUI/Store/ComponentsStore/PermissionComponentsStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionComponentsStore.swift 3 | // 4 | // 5 | // Created by Jevon Mao on 3/17/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /** 12 | The data storage class that contains configurable permission components 13 | */ 14 | @available(iOS 13.0, tvOS 13.0, *) 15 | public struct PermissionComponentsStore { 16 | //MARK: Creating a new permission components store 17 | /** 18 | Creates a new permission components store with default settings 19 | 20 | Use this initializer to declare a new instance of `PermissionComponentsStore`. Configure all the individual permission components, including image, title, and description by assigning to their properties. 21 | For example: 22 | ``` 23 | let store = PermissionStore() 24 | store.permissionComponentsStore.cameraPermission = JMPermission( 25 | imageIcon: AnyView(Image(systemName: "camera.fill")), 26 | title: "Camera", 27 | description: "Allow to use your camera", authorized: false) 28 | ``` 29 | */ 30 | public init(){} 31 | //MARK: Permission Components 32 | /// The displayed text and image icon for the biometrics permission 33 | public var biometricPermission = JMPermission( 34 | imageIcon: AnyView(Image(systemName: "faceid")), 35 | title: "Biometrics", 36 | description: "Allow to lock/hide your data from other persons" 37 | ) 38 | 39 | ///The displayed text and image icon for the camera permission 40 | public var cameraPermission = JMPermission( 41 | imageIcon: AnyView(Image(systemName: "camera.fill")), 42 | title: "Camera", 43 | description: "Allow to use your camera") 44 | ///The displayed text and image icon for the location permission 45 | public var locationPermission = JMPermission( 46 | imageIcon: AnyView(Image(systemName: "location.fill.viewfinder")), 47 | title: "Location", 48 | description: "Allow to access your location" 49 | ) 50 | ///The displayed text and image icon for the location always permission 51 | public var locationAlwaysPermission = JMPermission( 52 | imageIcon: AnyView(Image(systemName: "location.fill.viewfinder")), 53 | title: "Location Always", 54 | description: "Allow to access your location" 55 | ) 56 | ///The displayed text and image icon for the photo library permission 57 | public var photoPermission = JMPermission( 58 | imageIcon: AnyView(Image(systemName: "photo")), 59 | title: "Photo Library", 60 | description: "Allow to access your photos" 61 | ) 62 | ///The displayed text and image icon for the microphone permission 63 | public var microphonePermisson = JMPermission( 64 | imageIcon: AnyView(Image(systemName: "mic.fill")), 65 | title: "Microphone", 66 | description: "Allow to record with microphone" 67 | ) 68 | ///The displayed text and image icon for the notification center permission 69 | public var notificationPermission = JMPermission( 70 | imageIcon: AnyView(Image(systemName: "bell.fill")), 71 | title: "Notification", 72 | description: "Allow to send notifications" 73 | ) 74 | ///The displayed text and image icon for the calendar permission 75 | public var calendarPermisson = JMPermission( 76 | imageIcon: AnyView(Image(systemName: "calendar")), 77 | title: "Calendar", 78 | description: "Allow to access calendar" 79 | ) 80 | ///The displayed text and image icon for the bluetooth permission 81 | public var bluetoothPermission = JMPermission( 82 | imageIcon: AnyView(Image(systemName: "wave.3.left.circle.fill")), 83 | title: "Bluetooth", 84 | description: "Allow to use bluetooth" 85 | ) 86 | ///The displayed text and image icon for the permission to track across apps and websites 87 | public var trackingPermission = JMPermission( 88 | imageIcon: AnyView(Image(systemName: "person.circle.fill")), 89 | title: "Tracking", 90 | description: "Allow to track your data" 91 | ) 92 | ///The displayed text and image icon for the contact permission 93 | public var contactsPermission = JMPermission( 94 | imageIcon: AnyView(Image(systemName: "book.fill")), 95 | title: "Contacts", 96 | description: "Allow to access your contacts" 97 | ) 98 | ///The displayed text and image icon for the motion permission 99 | public var motionPermission = JMPermission( 100 | imageIcon: AnyView(Image(systemName: "hare.fill")), 101 | title: "Motion", 102 | description: "Allow to access your motion sensor data" 103 | ) 104 | ///The displayed text and image icon for the reminders permission 105 | public var remindersPermission = JMPermission( 106 | imageIcon: AnyView(Image(systemName: "list.bullet.rectangle")), 107 | title: "Reminders", 108 | description: "Allow to access your reminders" 109 | ) 110 | ///The displayed text and image icon for the speech recognition permission 111 | public var speechPermission = JMPermission( 112 | imageIcon: AnyView(Image(systemName: "rectangle.3.offgrid.bubble.left.fill")), 113 | title: "Speech", 114 | description: "Allow to access speech recognition") 115 | ///The displayed text and image icon for the health permission 116 | public var healthPermission = JMPermission( 117 | imageIcon: AnyView(Image(systemName: "heart.fill")), 118 | title: "health_title".localized, 119 | description: "health_description".localized) 120 | ///The displayed text and image icon for the music permission 121 | public var musicPermission = JMPermission(imageIcon: Image(systemName: "music.note.list").typeErased(), 122 | title: "Music", 123 | description: "Allow to control audio playback") 124 | ///The displayed text and image icon for the siri permission 125 | public var siriPermission = JMPermission(imageIcon: Image(systemName: "waveform").typeErased(), 126 | title: "Siri", 127 | description: "Allow Siri to interact with app") 128 | 129 | 130 | } 131 | 132 | @available(iOS 13.0, tvOS 13.0, *) 133 | extension PermissionComponentsStore { 134 | @usableFromInline 135 | @discardableResult 136 | mutating func getPermissionComponent(for permission: PermissionType, 137 | modify: (inout JMPermission) -> Void = {_ in}) -> JMPermission { 138 | switch permission { 139 | case .location: 140 | modify(&self.locationPermission) 141 | return self.locationPermission 142 | case .biometrics: 143 | modify(&self.biometricPermission) 144 | return self.biometricPermission 145 | case .locationAlways: 146 | modify(&self.locationAlwaysPermission) 147 | return self.locationAlwaysPermission 148 | case .photo: 149 | modify(&self.photoPermission) 150 | return self.photoPermission 151 | case .microphone: 152 | modify(&self.microphonePermisson) 153 | return self.microphonePermisson 154 | case .camera: 155 | modify(&self.cameraPermission) 156 | return self.cameraPermission 157 | case .notification: 158 | modify(&self.notificationPermission) 159 | return self.notificationPermission 160 | case .calendar: 161 | modify(&self.calendarPermisson) 162 | return self.calendarPermisson 163 | case .bluetooth: 164 | modify(&self.bluetoothPermission) 165 | return self.bluetoothPermission 166 | case .tracking: 167 | modify(&self.trackingPermission) 168 | return self.trackingPermission 169 | case .contacts: 170 | modify(&self.contactsPermission) 171 | return self.contactsPermission 172 | case .motion: 173 | modify(&self.motionPermission) 174 | return self.motionPermission 175 | case .reminders: 176 | modify(&self.remindersPermission) 177 | return self.remindersPermission 178 | case .speech: 179 | modify(&self.speechPermission) 180 | return self.speechPermission 181 | case .music: 182 | modify(&self.musicPermission) 183 | return self.musicPermission 184 | #if !os(tvOS) 185 | case .health: 186 | modify(&self.healthPermission) 187 | return self.healthPermission 188 | #endif 189 | case .siri: 190 | modify(&self.siriPermission) 191 | return self.siriPermission 192 | } 193 | } 194 | } 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | 7 | # PermissionsSwiftUI: A SwiftUI package to handle permissions 8 | 9 | 10 | `PermissionsSwiftUI` displays and handles permissions in SwiftUI. It is largely inspired by [SPPermissions](https://github.com/varabeis/SPPermissions). 11 | The UI is highly customizable and resembles an **Apple style**. If you like the project, please `star ★`.
12 |
13 |

14 | 15 |

16 |

PermissionsSwiftUI looks equally gorgeous on both ☀️light and 🌑 dark mode.

17 | 18 | ## 🧭 Navigation 19 | - [Installation](#-installation) 20 | - [Quickstart](#-quickstart) 21 |
22 | Usage 23 | 24 | - [Usage](#-usage) 25 | - [Customize Permission Texts](#customize-permission-texts) 26 | - [Customize header texts](#customize-header-texts) 27 | - [`onAppear` and `onDisappear` Override](#onappear-and-ondisappear-override) 28 | - [Auto Check Authorization](#auto-check-authorization) 29 | - [Auto Dismiss](#auto-dismiss) 30 | - [Customize Colors](#customize-colors) 31 | - [Restrict Dismissal](#restrict-dismissal) 32 | - [Configuring Health Permissions](#configuring-health-permissions) 33 |
34 | 35 | - [Cheatsheet](#-cheatsheat) 36 | - [Supported Permissions](#-supported-permissions) 37 | - [Contribute](#-contribute) 38 | 39 |
40 | Additional Information 41 | 42 | - [Additional Information](#additional-information) 43 | - [Acknowledgement](#acknowledgement) 44 | - [License](#license) 45 | 46 |
47 | 48 | ## 🖥️ Installation 49 | ### Requirements 50 | * iOS 11 (SwiftUI require iOS 13.0) or iPadOS 13 51 | * Xcode 12 and Swift 5.3 52 | * tvOS support coming soon 53 | * No MacOS, and WatchOS support for now 54 | 55 | ### Install 56 | #### Swift Package Manager (Recommended) 57 | You can install PermissionsSwiftUI into your Xcode project via SPM. 58 | To learn more about SPM, click [here](https://swift.org/package-manager/) 59 | 1. In Xcode 12, open your project and navigate to **File** → **Swift Packages** → **Add Package Dependency...** 60 | 61 | For Xcode 13, navigate to **Files** → **Add Package** 62 | 63 | 2. Paste the repository URL (`https://github.com/jevonmao/PermissionsSwiftUI`) and click **Next**. 64 | 3. For **Version**, verify it's **Up to next major**. 65 | 4. Click **Next** and ONLY SELECT PERMISSIONS NEEDED else Apple will reject your app 66 | 67 | (You don't need to add CorePermissionsSwiftUI or PermissionsSwiftUI) 68 | 69 | image 70 | 71 | 5. Click **Finish** 72 | 6. You are all set, thank you for using PermissionsSwiftUI! 73 | 74 | 75 | #### Cocoapods (Deprecated) 76 | You can also install PermissionsSwiftUI with Cocoapods. Add `pod 'PermissionsSwiftUI'` in your podfile: 77 | ```Ruby 78 | platform :ios, '14.0' 79 | 80 | target 'test abstract' do 81 | use_frameworks! 82 | pod 'PermissionsSwiftUI' 83 | 84 | end 85 | ``` 86 | ## 🚀 Quickstart 87 | > Before you start, please `star ★` this repository. Your star is my biggest motivation to pull all-nighters and maintain this open-source project. 88 | 89 | ### ⚠️ v1.4.0 Migration Guide 90 | `v1.4` is here! If you encounter any issues, please check out the [migration guide](https://github.com/jevonmao/PermissionsSwiftUI/wiki/Migrating-to-v1.4.0) designed to help developers resolve any deprecations and API updates. 91 | 92 | ### Modal Style 93 | To use PermissionsSwiftUI, simply add the `JMModal` modifier to any view: 94 | ```Swift 95 | .JMModal(showModal: $showModal, for: [.locationAlways, .photo, .microphone])` 96 | ``` 97 | Pass in a `Binding` to show the modal view, and add whatever permissions you want to show. For example: 98 | ```Swift 99 | struct ContentView: View { 100 | @State var showModal = false 101 | var body: some View { 102 | Button(action: { 103 | showModal=true 104 | }, label: { 105 | Text("Ask user for permissions") 106 | }) 107 | .JMModal(showModal: $showModal, for: [.locationAlways, .photo, .microphone]) 108 | } 109 | } 110 | ``` 111 | ### Alert Style 112 | 113 | The alert style is equally gorgeous, and allows for more versatile use. It is recommended when you have less than 3 permissions.
114 | To show a permission pop up alert, use: 115 | 116 | ```Swift 117 | .JMAlert(showModal: $showModal, for: [.locationAlways, .photo]) 118 | ``` 119 | Similar to the previous `JMPermissions`, you need to pass in a `Binding` to show the view, and add whatever permissions you want to show. 120 | To quickly glance at all of PermissionsSwiftUI's customization and configurations, check out the [cheatsheet](#cheatsheat)! 121 |





122 | 123 | ## 🛠️ Usage 124 | ### Customize Permission Texts 125 | 126 | To customize permission texts, use the modifier `setPermissionComponent()` 127 | For example, you can change title, description, and image icon: 128 | ```Swift 129 | .setPermissionComponent(for: .camera, 130 | image: AnyView(Image(systemName: "camera.fill")), 131 | title: "Camcorder", 132 | description: "App needs to record videos") 133 | ``` 134 | and the result: 135 |
136 | 137 |
138 |
139 | Or only change 1 of title and description: 140 | 141 | ```Swift 142 | setPermissionComponent(for: .tracking, title: "Trackers") 143 | ``` 144 | ```Swift 145 | setPermissionComponent(for: .tracking, description: "Tracking description") 146 | ``` 147 | 148 | **Note:** 149 | * The parameters you don't provide will show the default text 150 | * Add the `setPermissionComponent` modifier on your root level view, after `JMPermissions` modifier 151 | 152 | The `image` parameter accepts **AnyView**, so feel free to use [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/) or your custom asset: 153 | ```Swift 154 | .setPermissionComponent(for: .camera, 155 | image: AnyView(Image("Your-cool-image")) 156 | ``` 157 | Even full SwiftUI views will work😱: 158 | ```Swift 159 | .setPermissionComponent(for: .camera, 160 | image: AnyView(YourCoolView()) 161 | ``` 162 | You can use custom text and icons for all the supported permissions, with a single line of code. 163 | ### Customize Header Texts 164 | To customize the header title, use the modifier `changeHeaderTo`: 165 | Annotated for headers screen 166 | ```Swift 167 | .JMPermissions(showModal: $showModal, for: [.camera, .location, .calendar]) 168 | .changeHeaderTo("App Permissions") 169 | ``` 170 | To customize the header description, use the modifier `changeHeaderDescriptionTo`: 171 | ```Swift 172 | .JMPermissions(showModal: $showModal, for: [.camera, .location, .photo]) 173 | .changeHeaderDescriptionTo("Instagram need certain permissions in order for all the features to work.") 174 | ``` 175 | To customize the bottom description, use the modifier `changeBottomDescriptionTo`: 176 | ```Swift 177 | .JMPermissions(showModal: $showModal, for: [.camera, .location, .photo]) 178 | .changeBottomDescriptionTo("If not allowed, you have to enable permissions in settings") 179 | ``` 180 | ### `onAppear` and `onDisappear` Override 181 | You might find it incredibly useful to execute your code, or perform some update action when a PermissionsSwiftUI view appears and disappears.
182 | You can perform some action when PermissionsSwiftUI view appears or disappears by: 183 | ```Swift 184 | .JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone], onAppear: {}, onDisappear: {}) 185 | ``` 186 | The `onAppear` and `onDisappear` **closure parameters will be executed** everytime PermissionsSwiftUI view **appears and disappears.**
187 | The same view modifier closure for state changes are available for the `JMAlert` modifier: 188 | ```Swift 189 | .JMAlert(showModal: $showModal, 190 | for: [.locationAlways, .photo], 191 | onAppear: {print("Appeared")}, 192 | onDisappear: {print("Disappeared")}) 193 | ``` 194 | ### Auto Check Authorization 195 | PermissionsSwiftUI by default will automatically check for authorization status. It will only show permissions that are currently `notDetermined` status. (the iOS system prevents developers from asking for denied permissions. Allowed permissions will also be ignored by PermissionsSwiftUI). If all permissions are allowed or denied, PermissionsSwiftUI will not show the modal or alert at all. 196 | To set auto check authorization, use the `autoCheckAuthorization` parameter: 197 | ```Swift 198 | .JMModal(showModal: $showModal, for: [.camera], autoCheckAuthorization: false) 199 | ``` 200 | same applies for JMAlert 201 | ```Swift 202 | .JMAlert(showModal: $showModal, for: [.camera], autoCheckAuthorization: false) 203 | ``` 204 | ### Auto Dismiss 205 | PermissionsSwiftUI by default will not have any auto dismiss behavior. You can override this behavior to make it automatically dismiss the modal or alert after the user allows the last permission item. (All permissions must be ALLOWED, if any is DENIED, it will not auto dismiss). 206 | ```Swift 207 | .JMModal(... autoDismiss: Bool) -> some View 208 | ``` 209 | Pass in `true` or `false` to select whether to automatically dismiss the view. 210 | 211 | ### Customize Colors 212 | Using PermissionSwiftUI's capabilities, developers and designers can customize all the UI colors with incredible flexibility. You can fully configure all color at all states with your custom colors.
213 | To easily change the accent color: 214 | ```Swift 215 | .setAccentColor(to: Color(.sRGB, red: 56/255, green: 173/255, 216 | blue: 169/255, opacity: 1)) 217 | ``` 218 | To change the primary (default Apple blue) and tertiary (default Apple red) colors: 219 | ```Swift 220 | .setAccentColor(toPrimary: Color(.sRGB, red: 56/255, green: 173/255, 221 | blue: 169/255, opacity: 1), 222 | toTertiary: Color(.systemPink)) 223 | ``` 224 |

225 | 226 |

227 | 228 | > ⚠️ `.setAccentColor()` and `.setAllowButtonColor()` should never be used at the same time. 229 | 230 | To unleash the full customization of all button colors under all states, you need to pass in the `AllButtonColors` struct: 231 | ```Swift 232 | .setAllowButtonColor(to: .init(buttonIdle: ButtonColor(foregroundColor: Color, 233 | backgroundColor: Color), 234 | buttonAllowed: ButtonColor(foregroundColor: Color, 235 | backgroundColor: Color), 236 | buttonDenied: ButtonColor(foregroundColor: Color, 237 | backgroundColor: Color))) 238 | ``` 239 | For more information regarding the above method, reference the [official documentation](https://jevonmao.github.io/PermissionsSwiftUI/Structs/AllButtonColors.html). 240 | 241 | ### Restrict Dismissal 242 | PermissionsSwiftUI will by default, prevent the user from dismissing the modal and alert before all permissions have been interacted with. This means if the user has not explicitly denied or allowed EVERY permission shown, they will not be able to dismiss the PermissionsSwiftUI view. This restricts dismissal behavior can be overridden by the `var restrictModalDismissal: Bool` or `var restrictAlertDismissal: Bool` properties. 243 | To disable the default restrict dismiss behavior: 244 | ```Swift 245 | .JMModal(showModal: $show, for permissions: [.camera], restrictDismissal: false) 246 | ``` 247 | You can also configure with the model: 248 | ```Swift 249 | let model: PermissionStore = { 250 | var model = PermissionStore() 251 | model.permissions = [.camera] 252 | model.restrictModalDismissal = false 253 | model.restrictAlertDismissal = false 254 | return model 255 | } 256 | ...... 257 | 258 | .JMModal(showModal: $showModal, forModel: model) 259 | ``` 260 | ### Configuring Health Permissions 261 | Unlike all the other permissions, the configuration for health permission is a little different. Because Apple requires developers to explicitly set read and write types, PermissionsSwiftUI greatly simplifies the process. 262 | #### `HKAccess` 263 | The structure HKAccess is required when initializing health permission’s enum associated values. It encapsulates the read and write type permissions for the health permission. 264 | 265 | To set read and write health types (`activeEnergyBurned` is used as example here): 266 | ```Swift 267 | let healthTypes = Set([HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!]) 268 | .JMModal(showModal: $show, for: [.health(categories: .init(readAndWrite: healthTypes))]) 269 | 270 | //Same exact syntax for JMAlert styles 271 | .JMAlert(showModal: $show, for: [.health(categories: .init(readAndWrite: healthTypes))]) 272 | 273 | ``` 274 | To set read or write individually: 275 | ```Swift 276 | let readTypes = Set([HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!]) 277 | let writeTypes = Set([HKSampleType.quantityType(forIdentifier: .appleStandTime)!]) 278 | .JMModal(showModal: $showModal, for: [.health(categories: .init(read: readTypes, write: writeTypes))]) 279 | ``` 280 | You may also set only read or write type: 281 | ```Swift 282 | let readTypes = Set([HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!]) 283 | .JMModal(showModal: $showModal, for: [.health(categories: .init(read: readTypes))]) 284 | 285 | ``` 286 | ## 📖 Cheatsheet 287 | 288 | ### Modifiers 289 | **Customize overall accent color:** 290 | ```Swift 291 | setAccentColor(to:) 292 | setAccentColor(toPrimary:toTertiary:) 293 | ``` 294 | **Customize title:** 295 | ```Swift 296 | changeHeaderTo(_:) 297 | ``` 298 | **Customize top description:** 299 | ```Swift 300 | changeHeaderDescriptionTo(_:) 301 | ``` 302 | **Customize bottom description:** 303 | ```Swift 304 | changeBottomDescriptionTo(_:) 305 | ``` 306 | **Customize each permission's displayed text & image:** 307 | ```Swift 308 | setPermissionComponent(for:image:title:description:) 309 | 310 | setPermissionComponent(for:title:) 311 | 312 | setPermissionComponent(for:description:) 313 | ``` 314 | **Customize `allow` button's colors:** 315 | ```Swift 316 | setAllowButtonColor(to:) 317 | ``` 318 | **Automatically dismiss after last** 319 | ```Swift 320 | autoDismiss: Bool 321 | ``` 322 | ### Parameters of `JMModal` and `JMAlert` 323 | **Check authorization before showing modal or alert** 324 | ```Swift 325 | autoCheckAuthorization: Bool 326 | ``` 327 | **Prevent dismissing before all permissions interacted** 328 | ```Swift 329 | restrictDismissal: Bool 330 | ``` 331 | **Do something right before view appear** 332 | ```Swift 333 | onAppear: () -> Void 334 | ``` 335 | **Do something right before view disappear** 336 | ```Swift 337 | onDisappear: (() -> Void 338 | ``` 339 | ## 🧰 Supported Permissions 340 | Here is a list of all the permissions PermissionsSwiftUI supports. Yup, even the newest `tracking` permission for iOS 14 so you can stay on top of your game. All permissions in PermissionsSwiftUI come with a default name, description, and a stunning Apple native SF Symbols icon. 341 | 342 | Support for FaceID permission is work in progress and coming soon! If you don't find a permission you need, open an issue. Even better, build it yourself and open a pull request, you can follow [this](docs/New_Permission_Guide.md) step-by-step guide on adding new permissions. 343 |


344 | A card of all the permissions 345 | 346 | ## 💪 Contribute 347 | Contributions are welcome here for coders and non-coders alike. No matter what your skill level is, you can for certain contribute to PermissionSwiftUI's open source community. Please read [contributing.md](CONTRIBUTING.md) before starting, and if you are looking to contributing a new type of iOS permission, be sure to read this step-by-step [guide](docs/New_Permission_Guide.md). 348 | 349 | **If you encounter ANY issue, have ANY concerns, or ANY comments, please do NOT hesitate to let me know. Open a discussion, issue, or email me.** As a developer, I feel you when you don't understand something in the codebase. I try to comment and document as best as I can, but if you happen to encounter any issues, I will be happy to assist in any way I can. 350 | 351 | ## Additional Information 352 | 353 | ### Acknowledgement 354 | SPPermissions is in large a SwiftUI remake of the famous Swift library **[SPPermissions](https://github.com/varabeis/SPPermissions)** by @verabeis. SPPermissions was initially created in 2017, and today on GitHub has over 4000 stars. PermissionsSwiftUI aims to deliver a just as beautiful and powerful library in SwiftUI. If you `star ★` my project PermissionsSwiftUI, be sure to check out the original project SPPermissions where I borrowed the UI Design, some parts of README.md page, and important source code references along the way. 355 | ### License 356 | PermissionsSwiftUI is created by Jingwen (Jevon) Mao and licensed under the [MIT License](https://jingwen-mao.mit-license.org) 357 | 358 | --------------------------------------------------------------------------------