diff --git a/.github/workflows/on_pull_request.yml b/.github/workflows/on_pull_request.yml new file mode 100644 index 0000000..4a66c29 --- /dev/null +++ b/.github/workflows/on_pull_request.yml @@ -0,0 +1,70 @@ +name: Prepare pull request to merge + +on: + pull_request: + branches: + - main + paths: + - '.github/**' + - 'fastlane/**' + - 'SoundModeManager/**' + - 'SoundModeManagerTests/**' + +jobs: + update_version: + name: Update project version + runs-on: macOS-latest + timeout-minutes: 10 + steps: + + - name: Checkout pull request branch + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true + + - name: Update Xcode and podspec version + run: | + bundle exec fastlane update_version + + run_tests: + name: Build project and run tests + runs-on: macOS-latest + timeout-minutes: 30 + steps: + + - name: Checkout pull request branch + uses: actions/checkout@v2 + + - name: Select latest available version of Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Setup ruby and bundler + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true + + - name: Build project for iOS + run: | + bundle exec fastlane build_and_test + + check_pod_linter: + name: Check Cocoa Pod linter + runs-on: macOS-latest + timeout-minutes: 10 + steps: + + - name: Checkout pull request branch + uses: actions/checkout@v2 + + - name: Check Pod linter + run: | + pod lib lint \ No newline at end of file diff --git a/.github/workflows/on_pull_request_merged.yml b/.github/workflows/on_pull_request_merged.yml new file mode 100644 index 0000000..048716b --- /dev/null +++ b/.github/workflows/on_pull_request_merged.yml @@ -0,0 +1,31 @@ +name: Pull request has already merged + +on: + pull_request: + branches: + - main + types: + - closed + +jobs: + create_tag: + name: Create git tag based on project version + if: ${{ github.event.pull_request.merged }} + runs-on: macOS-latest + timeout-minutes: 30 + steps: + + - name: Checkout pull request branch + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.base.ref }} + + - name: Setup ruby and bundler + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true + + - name: Prepare project to deploy + run: | + bundle exec fastlane prepare_to_deploy \ No newline at end of file diff --git a/.github/workflows/on_release_created.yml b/.github/workflows/on_release_created.yml new file mode 100644 index 0000000..ef570bd --- /dev/null +++ b/.github/workflows/on_release_created.yml @@ -0,0 +1,24 @@ +name: Prepare project and deploy to services + +on: + release: + types: + - published + +jobs: + deploy_pod: + name: Deploy to Cocoa Pods + runs-on: macOS-latest + timeout-minutes: 30 + steps: + + - name: Checkout pull request branch + uses: actions/checkout@v2 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Publish pod to trunk + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_PASSWORD }} + run: | + pod trunk push \ No newline at end of file diff --git a/.gitignore b/.gitignore index 330d167..c183e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Mac OS X +*.DS_Store + # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore diff --git a/.swiftlint b/.swiftlint new file mode 100755 index 0000000..6837f76 Binary files /dev/null and b/.swiftlint differ diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..8aeee32 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,123 @@ +reporter: "xcode" + +# You can read more about rules +# https://realm.github.io/SwiftLint/rule-directory.html + +warning_threshold: 15 + +# Disabled rules from runnig +disabled_rules: + - identifier_name + - trailing_whitespace + - cyclomatic_complexity + - valid_ibinspectable + +# Opt in rules for runnig +opt_in_rules: + - closure_end_indentation + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - empty_collection_literal + - empty_count + - empty_string + - explicit_init + - fatal_error_message + - file_name + - file_name_no_space + - first_where + - implicit_return + - joined_default_parameter + - last_where + - literal_expression_end_indentation + - multiline_literal_brackets + - operator_usage_whitespace + - overridden_super_call + - prefer_zero_over_explicit_init + - redundant_type_annotation + - sorted_first_last + - toggle_bool + - unneeded_parentheses_in_closure_argument + - untyped_error_in_catch + - yoda_condition + +# Paths to ignore during linting. Takes precedence over `included`. +excluded: + - Pods + - Jodle/Resources/R.generated.swift + - Chat + - DerivedData + +# Configured length rules +line_length: + warning: 170 + ignores_comments: true + +type_body_length: + warning: 300 + error: 400 + +function_body_length: + warning: 60 + error: 100 + +file_length: + warning: 500 + error: 800 + ignore_comment_only_lines: true + +type_name: + min_length: 3 + max_length: + warning: 50 + error: 60 + +# Configured rules +colon: + apply_to_dictionaries: false + +nesting: + type_level: 2 + +empty_count: + severity: warning + only_after_dot: true + +# Configure custom_rules +custom_rules: + final_class: + severity: warning + name: "Every class must have \"final\" modifier" + message: "Add \"final\" modifier otherwise add the prefix \"Base\" to the class name or mark it as \"internal\" if this class will be overridden." + regex: "^(class) (?!Base)" + included: ".*.swift" + + mark_style: + severity: warning + name: "Use \"// MARK: - \" instead \"// MARK: \"" + regex: "MARK: [^-]{1}" + match_kinds: + - comment + included: ".*.swift" + + mark_newlines: + severity: warning + message: "Every \"MARK: -\" should be surrounded with newline before and after" + regex: "(([a-zA-Z0-9_}{)]+[ \t]*\n{1}[ \t]*)([\/]{2} MARK: - [a-zA-Z0-9 ]*))|(([\/]{2} MARK: - [a-zA-Z0-9 ]*)([\n]{1}[ \t]*[a-zA-z0-9_]+))" + included: ".*.swift" + + mark_extensions: + severity: warning + message: "Every extension group should have \"MARK: - \" with name of this extension below" + regex: "([}]+[\t]*\n{1}[ \t]*)(?![\/]{2} MARK: - [a-zA-Z0-9 ]*)([\n]+[ \t]*)(extension [a-zA-Z0-9_]+:)" + included: ".*.swift" + + empty_closure_params: + included: ".*.swift" + name: "Empty closure should be avoided" + regex: "[{]([\n\t ]*_ in[\n\t ]*)[}]" + message: "Use optional closures instead" + severity: warning \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..cab2b38 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,214 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.4) + rexml + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.518.0) + aws-sdk-core (3.121.3) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.50.0) + aws-sdk-core (~> 3, >= 3.121.2) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.104.0) + aws-sdk-core (~> 3, >= 3.121.2) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.4.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.2.3) + excon (0.87.0) + faraday (1.8.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.5) + fastlane (2.197.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.12.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-core (0.4.1) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.7.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.5.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.8.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.5.0) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.34.1) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.0.0) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.4) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.4.0) + json (2.6.0) + jwt (2.3.0) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.2) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.1) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.6) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.16.0) + addressable (~> 2.8) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8) + unicode-display_width (1.8.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.21.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 1.17.2 diff --git a/Preview.gif b/Preview.gif new file mode 100644 index 0000000..f46bcb8 Binary files /dev/null and b/Preview.gif differ diff --git a/README.md b/README.md index a19c0fa..b225251 100644 --- a/README.md +++ b/README.md @@ -1 +1,93 @@ -# SoundModeManager \ No newline at end of file +# 📴 SoundModeManager 📳 +> Detect silent / ring mode on the device. + +[![Languages](https://img.shields.io/github/languages/top/yurii-lysytsia/SoundModeManager?color=orange)]() +[![Platforms](https://img.shields.io/cocoapods/p/SoundModeManager)]() +[![CocoaPods](https://img.shields.io/cocoapods/v/SoundModeManager?color=red)]() +[![Build](https://img.shields.io/github/workflow/status/yurii-lysytsia/SoundModeManager/Prepare%20pull%20request%20to%20merge)]() + +[![Preview](Preview.gif)]() + +- [Installation](#-installation) + - [CocoaPods](#cocoapods) +- [Usage](#-usage) +- [Documentation](#-documentation) +- [License](#-license) + +## 🚀 Installation + +### [CocoaPods](https://cocoapods.org) +For usage and installation instructions, visit their website. To integrate AirKit into your Xcode project using CocoaPods, specify it in your `Podfile`: +```ruby +pod 'SoundModeManager' +``` + +## 💻 Usage +Sound mode manager doesn't work on simulators! + +### Create a new instance of manager: +```swift +import SoundModeManager + +// Fully customized instance. +let manager = SoundModeManager(soundUrl: customSoundUrl, soundUpdatingInterval: 5) + +// With custom sound file only. +let manager = SoundModeManager(soundUrl: customSoundUrl) + +// With default silent sound and custom updating interval. +let manager = SoundModeManager(soundUpdatingInterval: 5) + +// Default manager configuration. +let manager = SoundModeManager() +``` + +### Update current mode once (not recommended): +```swift +import SoundModeManager + +let manager = SoundModeManager() + +// Mode is not determined by default. +manager.currentMode // SoundMode.notDetermined + +// Update current mode and receive callback. +manager.updateCurrentMode { mode in + // Mode is `.silent` or `.ring` + manager.currentMode == mode // true +} +``` + +### Observe current mode changes: +```swift +import SoundModeManager + +let manager = SoundModeManager() + +// Mode is not determined by default. +manager.currentMode // SoundMode.notDetermined + +// Save token to manage observer and subscribe to receive changes. +let observationToken = sut.observeCurrentMode { mode in + // Block will be called only when new mode is not the same as previous. + // Mode is `.silent` or `.ring`. + manager.currentMode == mode // true +} + +// Start observing current mode. +manager.beginUpdatingCurrentMode() + +// End observing current mode. This method suspend all notification, but all observers are still valid. +manager.endUpdatingCurrentMode() +``` + +### Invalidate observation token: +```swift +// Invalidate observation token is working the same as `NSKeyValueObservation`; +// So you are able to invalidate it manually if you need; +// Token will be invalidated automatically when it is deinited. +observationToken.invalidate() +``` + +## 📜 License +Released under the MIT license. See [LICENSE](LICENSE) for details. diff --git a/SoundModeManager.podspec b/SoundModeManager.podspec new file mode 100644 index 0000000..6b768bc --- /dev/null +++ b/SoundModeManager.podspec @@ -0,0 +1,29 @@ +# To learn more about Podspec attributes see https://guides.cocoapods.org/syntax/podspec.html + +Pod::Spec.new do |spec| + + # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + spec.name = "SoundModeManager" + spec.version = "1.0.0" + spec.summary = "Detect silent / ring mode on the iOS device" + spec.description = "This is framework to detect silent / ring mode on the iOS device written on Swift" + spec.homepage = "https://github.com/yurii-lysytsia/SoundModeManager" + + # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + spec.license = { :type => "MIT", :file => "LICENSE" } + + # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + spec.author = { "Yurii Lysytsia" => "developer.yurii.lysytsia@gmail.com" } + spec.social_media_url = "https://www.yurii-lysytsia.site" + + # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + spec.ios.deployment_target = "9.0" + spec.swift_versions = '5.0' + + # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + spec.source = { :git => "https://github.com/yurii-lysytsia/SoundModeManager.git", :tag => "#{spec.version}" } + + # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + spec.source_files = "SoundModeManager/Source/**/*.swift" + +end diff --git a/SoundModeManager.xcodeproj/project.pbxproj b/SoundModeManager.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6edf2e1 --- /dev/null +++ b/SoundModeManager.xcodeproj/project.pbxproj @@ -0,0 +1,796 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 5BD709C6272E8316006244C6 /* SoundModeManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BD709BD272E8316006244C6 /* SoundModeManager.framework */; }; + 5BD709CB272E8316006244C6 /* SoundModeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD709CA272E8316006244C6 /* SoundModeManagerTests.swift */; }; + 5BD709CC272E8316006244C6 /* SoundModeManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5BD709C0272E8316006244C6 /* SoundModeManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5BD709DD272E870C006244C6 /* SoundModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD709DC272E870C006244C6 /* SoundModeManager.swift */; }; + 5BD709DF272E8774006244C6 /* SoundMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD709DE272E8774006244C6 /* SoundMode.swift */; }; + 5BD709E5272E8A01006244C6 /* silent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 5BD709E4272E8A01006244C6 /* silent.aiff */; }; + 5BD70A10272EA7C2006244C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD70A0F272EA7C2006244C6 /* AppDelegate.swift */; }; + 5BD70A14272EA7C2006244C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD70A13272EA7C2006244C6 /* ViewController.swift */; }; + 5BD70A19272EA7C3006244C6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BD70A18272EA7C3006244C6 /* Assets.xcassets */; }; + 5BD70A22272EA85A006244C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BD70A21272EA85A006244C6 /* Main.storyboard */; }; + 5BD70A25272EA880006244C6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BD70A24272EA880006244C6 /* LaunchScreen.storyboard */; }; + 5BD70A2A272EAF7F006244C6 /* SoundModeManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BD709BD272E8316006244C6 /* SoundModeManager.framework */; platformFilter = ios; }; + 5BD70A2B272EAF7F006244C6 /* SoundModeManager.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5BD709BD272E8316006244C6 /* SoundModeManager.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 5BD709C7272E8316006244C6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5BD709B4272E8316006244C6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5BD709BC272E8316006244C6; + remoteInfo = SoundModeManager; + }; + 5BD70A2C272EAF7F006244C6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5BD709B4272E8316006244C6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5BD709BC272E8316006244C6; + remoteInfo = SoundModeManager; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 5BD70A2E272EAF7F006244C6 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5BD70A2B272EAF7F006244C6 /* SoundModeManager.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 5BD709BD272E8316006244C6 /* SoundModeManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SoundModeManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BD709C0272E8316006244C6 /* SoundModeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SoundModeManager.h; sourceTree = ""; }; + 5BD709C5272E8316006244C6 /* SoundModeManagerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SoundModeManagerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BD709CA272E8316006244C6 /* SoundModeManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundModeManagerTests.swift; sourceTree = ""; }; + 5BD709D7272E83AD006244C6 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 5BD709DC272E870C006244C6 /* SoundModeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundModeManager.swift; sourceTree = ""; }; + 5BD709DE272E8774006244C6 /* SoundMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundMode.swift; sourceTree = ""; }; + 5BD709E4272E8A01006244C6 /* silent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silent.aiff; sourceTree = ""; }; + 5BD709E7272EA38B006244C6 /* SoundModeManager.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = SoundModeManager.podspec; sourceTree = ""; }; + 5BD70A0D272EA7C2006244C6 /* SoundModeManagerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SoundModeManagerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BD70A0F272EA7C2006244C6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5BD70A13272EA7C2006244C6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 5BD70A18272EA7C3006244C6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5BD70A1D272EA7C3006244C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5BD70A21272EA85A006244C6 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + 5BD70A24272EA880006244C6 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 5BD70A30272EC43C006244C6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5BD709BA272E8316006244C6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BD709C2272E8316006244C6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD709C6272E8316006244C6 /* SoundModeManager.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BD70A0A272EA7C2006244C6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD70A2A272EAF7F006244C6 /* SoundModeManager.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5BD709B3272E8316006244C6 = { + isa = PBXGroup; + children = ( + 5BD709D7272E83AD006244C6 /* README.md */, + 5BD709E7272EA38B006244C6 /* SoundModeManager.podspec */, + 5BD709BF272E8316006244C6 /* SoundModeManager */, + 5BD709C9272E8316006244C6 /* SoundModeManagerTests */, + 5BD70A0E272EA7C2006244C6 /* SoundModeManagerExample */, + 5BD709BE272E8316006244C6 /* Products */, + 5BD70A29272EAF7F006244C6 /* Frameworks */, + ); + sourceTree = ""; + }; + 5BD709BE272E8316006244C6 /* Products */ = { + isa = PBXGroup; + children = ( + 5BD709BD272E8316006244C6 /* SoundModeManager.framework */, + 5BD709C5272E8316006244C6 /* SoundModeManagerTests.xctest */, + 5BD70A0D272EA7C2006244C6 /* SoundModeManagerExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 5BD709BF272E8316006244C6 /* SoundModeManager */ = { + isa = PBXGroup; + children = ( + 5BD709C0272E8316006244C6 /* SoundModeManager.h */, + 5BD709D9272E869E006244C6 /* Source */, + 5BD709E6272E8A0A006244C6 /* Resource */, + 5BD70A30272EC43C006244C6 /* Info.plist */, + ); + path = SoundModeManager; + sourceTree = ""; + }; + 5BD709C9272E8316006244C6 /* SoundModeManagerTests */ = { + isa = PBXGroup; + children = ( + 5BD709E2272E8894006244C6 /* Source */, + ); + path = SoundModeManagerTests; + sourceTree = ""; + }; + 5BD709D9272E869E006244C6 /* Source */ = { + isa = PBXGroup; + children = ( + 5BD709E0272E8777006244C6 /* Models */, + 5BD709E1272E877D006244C6 /* Managers */, + ); + path = Source; + sourceTree = ""; + }; + 5BD709E0272E8777006244C6 /* Models */ = { + isa = PBXGroup; + children = ( + 5BD709DE272E8774006244C6 /* SoundMode.swift */, + ); + path = Models; + sourceTree = ""; + }; + 5BD709E1272E877D006244C6 /* Managers */ = { + isa = PBXGroup; + children = ( + 5BD709DC272E870C006244C6 /* SoundModeManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; + 5BD709E2272E8894006244C6 /* Source */ = { + isa = PBXGroup; + children = ( + 5BD709E3272E88A8006244C6 /* Managers */, + ); + path = Source; + sourceTree = ""; + }; + 5BD709E3272E88A8006244C6 /* Managers */ = { + isa = PBXGroup; + children = ( + 5BD709CA272E8316006244C6 /* SoundModeManagerTests.swift */, + ); + path = Managers; + sourceTree = ""; + }; + 5BD709E6272E8A0A006244C6 /* Resource */ = { + isa = PBXGroup; + children = ( + 5BD709E4272E8A01006244C6 /* silent.aiff */, + ); + path = Resource; + sourceTree = ""; + }; + 5BD70A0E272EA7C2006244C6 /* SoundModeManagerExample */ = { + isa = PBXGroup; + children = ( + 5BD70A0F272EA7C2006244C6 /* AppDelegate.swift */, + 5BD70A23272EA877006244C6 /* Modules */, + 5BD70A18272EA7C3006244C6 /* Assets.xcassets */, + 5BD70A1D272EA7C3006244C6 /* Info.plist */, + ); + path = SoundModeManagerExample; + sourceTree = ""; + }; + 5BD70A23272EA877006244C6 /* Modules */ = { + isa = PBXGroup; + children = ( + 5BD70A26272EA884006244C6 /* LaunchScreen */, + 5BD70A27272EA965006244C6 /* Main */, + ); + path = Modules; + sourceTree = ""; + }; + 5BD70A26272EA884006244C6 /* LaunchScreen */ = { + isa = PBXGroup; + children = ( + 5BD70A24272EA880006244C6 /* LaunchScreen.storyboard */, + ); + path = LaunchScreen; + sourceTree = ""; + }; + 5BD70A27272EA965006244C6 /* Main */ = { + isa = PBXGroup; + children = ( + 5BD70A21272EA85A006244C6 /* Main.storyboard */, + 5BD70A13272EA7C2006244C6 /* ViewController.swift */, + ); + path = Main; + sourceTree = ""; + }; + 5BD70A29272EAF7F006244C6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 5BD709B8272E8316006244C6 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD709CC272E8316006244C6 /* SoundModeManager.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 5BD709BC272E8316006244C6 /* SoundModeManager */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BD709CF272E8316006244C6 /* Build configuration list for PBXNativeTarget "SoundModeManager" */; + buildPhases = ( + 5BD709B8272E8316006244C6 /* Headers */, + 5BD709B9272E8316006244C6 /* Sources */, + 5BD709D6272E8365006244C6 /* [SwiftLint] Run */, + 5BD709BA272E8316006244C6 /* Frameworks */, + 5BD709BB272E8316006244C6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SoundModeManager; + productName = SoundModeManager; + productReference = 5BD709BD272E8316006244C6 /* SoundModeManager.framework */; + productType = "com.apple.product-type.framework"; + }; + 5BD709C4272E8316006244C6 /* SoundModeManagerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BD709D2272E8316006244C6 /* Build configuration list for PBXNativeTarget "SoundModeManagerTests" */; + buildPhases = ( + 5BD709C1272E8316006244C6 /* Sources */, + 5BD709C2272E8316006244C6 /* Frameworks */, + 5BD709C3272E8316006244C6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5BD709C8272E8316006244C6 /* PBXTargetDependency */, + ); + name = SoundModeManagerTests; + productName = SoundModeManagerTests; + productReference = 5BD709C5272E8316006244C6 /* SoundModeManagerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 5BD70A0C272EA7C2006244C6 /* SoundModeManagerExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BD70A1E272EA7C3006244C6 /* Build configuration list for PBXNativeTarget "SoundModeManagerExample" */; + buildPhases = ( + 5BD70A09272EA7C2006244C6 /* Sources */, + 5BD70A28272EAA19006244C6 /* [SwiftLint] Run */, + 5BD70A0A272EA7C2006244C6 /* Frameworks */, + 5BD70A0B272EA7C2006244C6 /* Resources */, + 5BD70A2E272EAF7F006244C6 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 5BD70A2D272EAF7F006244C6 /* PBXTargetDependency */, + ); + name = SoundModeManagerExample; + productName = SoundModeManagerExample; + productReference = 5BD70A0D272EA7C2006244C6 /* SoundModeManagerExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5BD709B4272E8316006244C6 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1310; + LastUpgradeCheck = 1310; + ORGANIZATIONNAME = "Yurii Lysytsia"; + TargetAttributes = { + 5BD709BC272E8316006244C6 = { + CreatedOnToolsVersion = 13.1; + LastSwiftMigration = 1310; + }; + 5BD709C4272E8316006244C6 = { + CreatedOnToolsVersion = 13.1; + }; + 5BD70A0C272EA7C2006244C6 = { + CreatedOnToolsVersion = 13.1; + }; + }; + }; + buildConfigurationList = 5BD709B7272E8316006244C6 /* Build configuration list for PBXProject "SoundModeManager" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5BD709B3272E8316006244C6; + productRefGroup = 5BD709BE272E8316006244C6 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5BD709BC272E8316006244C6 /* SoundModeManager */, + 5BD709C4272E8316006244C6 /* SoundModeManagerTests */, + 5BD70A0C272EA7C2006244C6 /* SoundModeManagerExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5BD709BB272E8316006244C6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD709E5272E8A01006244C6 /* silent.aiff in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BD709C3272E8316006244C6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BD70A0B272EA7C2006244C6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD70A25272EA880006244C6 /* LaunchScreen.storyboard in Resources */, + 5BD70A22272EA85A006244C6 /* Main.storyboard in Resources */, + 5BD70A19272EA7C3006244C6 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 5BD709D6272E8365006244C6 /* [SwiftLint] Run */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[SwiftLint] Run"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/.swiftlint\" --config \"${SRCROOT}/.swiftlint.yml\" --path \"${SRCROOT}/SoundModeManager\"\n"; + }; + 5BD70A28272EAA19006244C6 /* [SwiftLint] Run */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[SwiftLint] Run"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/.swiftlint\" --config \"${SRCROOT}/.swiftlint.yml\" --path \"${SRCROOT}/SoundModeManagerExample\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5BD709B9272E8316006244C6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD709DF272E8774006244C6 /* SoundMode.swift in Sources */, + 5BD709DD272E870C006244C6 /* SoundModeManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BD709C1272E8316006244C6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD709CB272E8316006244C6 /* SoundModeManagerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BD70A09272EA7C2006244C6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD70A14272EA7C2006244C6 /* ViewController.swift in Sources */, + 5BD70A10272EA7C2006244C6 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 5BD709C8272E8316006244C6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5BD709BC272E8316006244C6 /* SoundModeManager */; + targetProxy = 5BD709C7272E8316006244C6 /* PBXContainerItemProxy */; + }; + 5BD70A2D272EAF7F006244C6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 5BD709BC272E8316006244C6 /* SoundModeManager */; + targetProxy = 5BD70A2C272EAF7F006244C6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 5BD709CD272E8316006244C6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 5BD709CE272E8316006244C6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 5BD709D0272E8316006244C6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = SoundModeManager/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.lysytsia.soundmodemanager; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5BD709D1272E8316006244C6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = SoundModeManager/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.lysytsia.soundmodemanager; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 5BD709D3272E8316006244C6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.lysytsia.SoundModeManagerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5BD709D4272E8316006244C6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.lysytsia.SoundModeManagerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 5BD70A1F272EA7C3006244C6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SoundModeManagerExample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.lysytsia.soundmodemanager.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5BD70A20272EA7C3006244C6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SoundModeManagerExample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.lysytsia.soundmodemanager.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5BD709B7272E8316006244C6 /* Build configuration list for PBXProject "SoundModeManager" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BD709CD272E8316006244C6 /* Debug */, + 5BD709CE272E8316006244C6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5BD709CF272E8316006244C6 /* Build configuration list for PBXNativeTarget "SoundModeManager" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BD709D0272E8316006244C6 /* Debug */, + 5BD709D1272E8316006244C6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5BD709D2272E8316006244C6 /* Build configuration list for PBXNativeTarget "SoundModeManagerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BD709D3272E8316006244C6 /* Debug */, + 5BD709D4272E8316006244C6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5BD70A1E272EA7C3006244C6 /* Build configuration list for PBXNativeTarget "SoundModeManagerExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BD70A1F272EA7C3006244C6 /* Debug */, + 5BD70A20272EA7C3006244C6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5BD709B4272E8316006244C6 /* Project object */; +} diff --git a/SoundModeManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SoundModeManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/SoundModeManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SoundModeManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SoundModeManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SoundModeManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SoundModeManager.xcodeproj/xcshareddata/IDETemplateMacros.plist b/SoundModeManager.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..425b2b2 --- /dev/null +++ b/SoundModeManager.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,8 @@ + + + + + FILEHEADER + ___COPYRIGHT___ + + diff --git a/SoundModeManager.xcodeproj/xcshareddata/xcschemes/SoundModeManager.xcscheme b/SoundModeManager.xcodeproj/xcshareddata/xcschemes/SoundModeManager.xcscheme new file mode 100644 index 0000000..4d3fdf8 --- /dev/null +++ b/SoundModeManager.xcodeproj/xcshareddata/xcschemes/SoundModeManager.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SoundModeManager.xcodeproj/xcshareddata/xcschemes/SoundModeManagerExample.xcscheme b/SoundModeManager.xcodeproj/xcshareddata/xcschemes/SoundModeManagerExample.xcscheme new file mode 100644 index 0000000..ce27913 --- /dev/null +++ b/SoundModeManager.xcodeproj/xcshareddata/xcschemes/SoundModeManagerExample.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SoundModeManager/Info.plist b/SoundModeManager/Info.plist new file mode 100644 index 0000000..7c5be14 --- /dev/null +++ b/SoundModeManager/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/SoundModeManager/Resource/silent.aiff b/SoundModeManager/Resource/silent.aiff new file mode 100644 index 0000000..c968a3c Binary files /dev/null and b/SoundModeManager/Resource/silent.aiff differ diff --git a/SoundModeManager/SoundModeManager.h b/SoundModeManager/SoundModeManager.h new file mode 100644 index 0000000..1de7e59 --- /dev/null +++ b/SoundModeManager/SoundModeManager.h @@ -0,0 +1,9 @@ +// Copyright © 2021 Yurii Lysytsia. All rights reserved. + +#import + +//! Project version number for SoundModeManager. +FOUNDATION_EXPORT double SoundModeManagerVersionNumber; + +//! Project version string for SoundModeManager. +FOUNDATION_EXPORT const unsigned char SoundModeManagerVersionString[]; diff --git a/SoundModeManager/Source/Managers/SoundModeManager.swift b/SoundModeManager/Source/Managers/SoundModeManager.swift new file mode 100644 index 0000000..e8238e2 --- /dev/null +++ b/SoundModeManager/Source/Managers/SoundModeManager.swift @@ -0,0 +1,279 @@ +// Copyright © 2021 Yurii Lysytsia. All rights reserved. + +#if canImport(AVFoundation) && canImport(UIKit) +import class AVFoundation.AVURLAsset +import struct AVFoundation.SystemSoundID +import var AVFoundation.kAudioServicesNoError +import var AVFoundation.kAudioServicesPropertyIsUISound +import func AVFoundation.AudioServicesCreateSystemSoundID +import func AVFoundation.AudioServicesSetProperty +import func AVFoundation.AudioServicesRemoveSystemSoundCompletion +import func AVFoundation.AudioServicesDisposeSystemSoundID +import func AVFoundation.AudioServicesPlaySystemSoundWithCompletion +import class UIKit.DispatchQueue +import class UIKit.UIApplication + +public class SoundModeManager: NSObject { + + // MARK: - Public Properties + + /// Current device's sound mode. Default value is `.notDetermined`. + /// + /// To receive the newest value you should call `updateCurrentMode()` method. + public private(set) var currentMode: SoundMode = .notDetermined + + // MARK: - Properties [Private] + + /// Weak objects hash table to collect observation tokens. + private var observationTokens = NSHashTable.weakObjects() + + /// Returns `true` if sound is currently playing. + private var isPlaying = false + + /// Returns `true` if updating mode timer is scheduled. + private var isObserved = false + + /// Updating current mode timer + private var updatingModeTimer: DispatchSourceTimer? + + /// Time difference between start and finish of mute sound. + private var lastPlaySoundTimeInterval: TimeInterval? + + /// Queue to use when sound playing is scheduled. + private let updatingQueue = DispatchQueue(label: "\(SoundModeManager.self).updatingQueue", qos: .utility) + + // MARK: - Properties [Observers] + + /// Notification token holder app enters background. + private var didEnterBackgroundNotificationToken: NSObjectProtocol? + + /// Notification token holder app enters foreground. + private var willEnterForegroundNotificationToken: NSObjectProtocol? + + // MARK: - Properties [Dependencies] + + /// Sound to check sound mode mode. + private let soundId: SystemSoundID + + /// Sound ID for mute sound. + private let soundUrl: URL + + /// Sound duration in seconds. + private let soundDuration: TimeInterval + + /// Frequency of checking the status in seconds. + private let soundUpdatingInterval: TimeInterval + + /// Weak dependency to notification center. + private unowned let notificationCenter: NotificationCenter + + // MARK: - Inits + + /// Create a new instance with given sound URL and other parameters. + /// - Parameters: + /// - soundUrl: URL to the local muted sound file. Sound should be muted. + /// - soundUpdatingInterval: Frequency of checking the status in seconds. Should be greater than `1`. Default is `1`. + /// - notificationCenter: Notification center dependency to use for observation. Default is `.default`. + public init(soundUrl: URL, soundUpdatingInterval: TimeInterval = 1, notificationCenter: NotificationCenter = .default) throws { + // Create a new system sound with given sound URL. + var soundId: SystemSoundID = 1 + var errorCode = AudioServicesCreateSystemSoundID(soundUrl as CFURL, &soundId) + + if errorCode != kAudioServicesNoError { + let userInfo: [String: Any]? + if #available(iOS 11.3, *) { + userInfo = [NSLocalizedDescriptionKey: SecCopyErrorMessageString(errorCode, nil) as Any] + } else { + userInfo = nil + } + + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errorCode), userInfo: userInfo) + } + + // Configure the new audio system sound to play only in `ring` mode. + var propertyFlag: UInt32 = 1 + errorCode = AudioServicesSetProperty( + kAudioServicesPropertyIsUISound, + UInt32(MemoryLayout.size(ofValue: soundId)), + &soundId, + UInt32(MemoryLayout.size(ofValue: propertyFlag)), + &propertyFlag + ) + + if errorCode != kAudioServicesNoError { + let userInfo: [String: Any]? + if #available(iOS 11.3, *) { + userInfo = [NSLocalizedDescriptionKey: SecCopyErrorMessageString(errorCode, nil) as Any] + } else { + userInfo = nil + } + + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errorCode), userInfo: userInfo) + } + + // Create sound asset to get duration. + let soundAsset = AVURLAsset(url: soundUrl) + + // Set required properties. + self.soundId = soundId + self.soundUrl = soundUrl + self.soundDuration = soundAsset.duration.seconds + self.soundUpdatingInterval = max(soundUpdatingInterval, 1) + self.notificationCenter = notificationCenter + super.init() + + // Observe background/foreground notifications. + didEnterBackgroundNotificationToken = notificationCenter.addObserver( + forName: UIApplication.didEnterBackgroundNotification, + object: nil, + queue: nil + ) { [weak self] _ in + self?.invalidateObservationTimer() + } + + willEnterForegroundNotificationToken = notificationCenter.addObserver( + forName: UIApplication.willEnterForegroundNotification, + object: nil, + queue: nil + ) { [weak self] _ in + guard let self = self, self.isObserved else { return } + self.startObservationTimer() + } + } + + /// Create a new instance with given parameters. Default silent sound URL will be used. + /// + /// - Parameters: + /// - soundUpdatingInterval: Frequency of checking the status in seconds. Should be greater than `1`. Default is `1`. + /// - notificationCenter: Notification center dependency to use for observation. Default is `.default`. + public convenience init(soundUpdatingInterval: TimeInterval = 1, notificationCenter: NotificationCenter = .default) { + guard let soundUrl = Bundle(for: SoundModeManager.self).url(forResource: "silent", withExtension: "aiff") else { + fatalError("\(#function) - \(SoundModeManager.self) couldn't find default silent sound `silent.aiff` in resources") + } + + do { + try self.init(soundUrl: soundUrl, soundUpdatingInterval: soundUpdatingInterval, notificationCenter: notificationCenter) + } catch { + fatalError("\(#function) - \(SoundModeManager.self) couldn't create a new instance with default sound URL. Error: \(error)") + } + } + + deinit { + // Remove the custom system sound and completion. + AudioServicesRemoveSystemSoundCompletion(soundId) + AudioServicesDisposeSystemSoundID(soundId) + + // Remove notification center observers. + [didEnterBackgroundNotificationToken, willEnterForegroundNotificationToken] + .compactMap { $0 } + .forEach { notificationCenter.removeObserver($0) } + } + + // MARK: - Methods + + /// Update current mode once and notify all observers. + public func updateCurrentMode(completion: ChangeHandler? = nil) { + playSound(completion: completion) + } + + /// Subscribe to receive `currentMode` notifications. + public func observeCurrentMode(changeHandler: @escaping ChangeHandler) -> ObservationToken { + let token = ObservationToken() + token.block = changeHandler + token.manager = self + observationTokens.add(token) + return token + } + + /// Begin updating current mode, `updateCurrentMode()` method will be called after every `soundCheckInterval` expiration. + public func beginUpdatingCurrentMode() { + startObservationTimer() + isObserved = true + } + + /// Finish updating current mode. + public func endUpdatingCurrentMode() { + invalidateObservationTimer() + guard isObserved else { return } + isObserved = false + } + + // MARK: - Helpers + + public typealias ChangeHandler = (SoundMode) -> Void + + /// Unique observation token to observe sound mode changes. + public final class ObservationToken: NSObject { + fileprivate var block: ChangeHandler? + fileprivate weak var manager: SoundModeManager? + + fileprivate override init() { } + + deinit { + invalidate() + } + + /// Invalidate current observation token. Method will be called automatically when an `ObservationToken` is deinited. + public func invalidate() { + manager?.observationTokens.remove(self) + } + } + +} + +// MARK: - Private + +private extension SoundModeManager { + /// Plays an added muted system sound. + func playSound(completion: ChangeHandler? = nil) { + if isPlaying { return } + + lastPlaySoundTimeInterval = Date.timeIntervalSinceReferenceDate + isPlaying = true + + AudioServicesPlaySystemSoundWithCompletion(soundId) { [weak self] in + guard let self = self, let lastPlaySoundTimeInterval = self.lastPlaySoundTimeInterval else { return } + self.isPlaying = false + self.lastPlaySoundTimeInterval = nil + + // Calculate new mode. + let elapsed = (Date.timeIntervalSinceReferenceDate - lastPlaySoundTimeInterval) < self.soundDuration + let newMode: SoundMode = elapsed ? .silent : .ring + + // Update mode and notify observers if needed. + if newMode != self.currentMode { + self.currentMode = newMode + DispatchQueue.main.async { + // Notify all observers. + self.observationTokens.allObjects.forEach { $0.block?(newMode) } + } + } + + // Notify current caller if exist. + DispatchQueue.main.async { + completion?(newMode) + } + } + } + + /// Creates a new timer and schedule it. + func startObservationTimer() { + guard updatingModeTimer == nil else { return } + + // Create a new timer and resume it. + let timer = DispatchSource.makeTimerSource(queue: updatingQueue) + timer.schedule(deadline: .now() + soundUpdatingInterval, repeating: soundUpdatingInterval) + timer.setEventHandler { [weak self] in + self?.playSound() + } + updatingModeTimer = timer + timer.resume() + } + + /// Invalidates and remove scheduled timer. + func invalidateObservationTimer() { + updatingModeTimer?.cancel() + updatingModeTimer = nil + } +} +#endif diff --git a/SoundModeManager/Source/Models/SoundMode.swift b/SoundModeManager/Source/Models/SoundMode.swift new file mode 100644 index 0000000..df5d210 --- /dev/null +++ b/SoundModeManager/Source/Models/SoundMode.swift @@ -0,0 +1,13 @@ +// Copyright © 2021 Yurii Lysytsia. All rights reserved. + +/// Sound mode on your iPhone. +public enum SoundMode: Int { + /// Sound mode not determined yet. + case notDetermined + + /// You don't hear ringtones and alerts, but your iPhone can still play sounds, like when you play music or videos. + case silent + + /// You hear ringtones and alerts. + case ring +} diff --git a/SoundModeManagerExample/AppDelegate.swift b/SoundModeManagerExample/AppDelegate.swift new file mode 100644 index 0000000..46f1cee --- /dev/null +++ b/SoundModeManagerExample/AppDelegate.swift @@ -0,0 +1,14 @@ +// Copyright © 2021 Yurii Lysytsia. All rights reserved. + +import UIKit + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + true + } + +} diff --git a/SoundModeManagerExample/Assets.xcassets/AccentColor.colorset/Contents.json b/SoundModeManagerExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/SoundModeManagerExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SoundModeManagerExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/SoundModeManagerExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/SoundModeManagerExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SoundModeManagerExample/Assets.xcassets/Contents.json b/SoundModeManagerExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SoundModeManagerExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SoundModeManagerExample/Info.plist b/SoundModeManagerExample/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/SoundModeManagerExample/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/SoundModeManagerExample/Modules/LaunchScreen/LaunchScreen.storyboard b/SoundModeManagerExample/Modules/LaunchScreen/LaunchScreen.storyboard new file mode 100644 index 0000000..03c3e34 --- /dev/null +++ b/SoundModeManagerExample/Modules/LaunchScreen/LaunchScreen.storyboard @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SoundModeManagerExample/Modules/Main/Main.storyboard b/SoundModeManagerExample/Modules/Main/Main.storyboard new file mode 100644 index 0000000..8ac546e --- /dev/null +++ b/SoundModeManagerExample/Modules/Main/Main.storyboard @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SoundModeManagerExample/Modules/Main/ViewController.swift b/SoundModeManagerExample/Modules/Main/ViewController.swift new file mode 100644 index 0000000..41d1595 --- /dev/null +++ b/SoundModeManagerExample/Modules/Main/ViewController.swift @@ -0,0 +1,81 @@ +// Copyright © 2021 Yurii Lysytsia. All rights reserved. + +import UIKit +import SoundModeManager + +final class ViewController: UIViewController { + + // MARK: - Public Properties + + override var prefersStatusBarHidden: Bool { true } + + // MARK: - Private Properties + + private var isObserved = false + private var observationToken: SoundModeManager.ObservationToken? + + // MARK: - Dependencies + + private let manager = SoundModeManager() + + // MARK: - Outlets + + @IBOutlet private weak var soundModeLabel: UILabel! + @IBOutlet private weak var observationButton: UIButton! + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + observeSoundMode() + setupView() + } + +} + +// MARK: - Private + +private extension ViewController { + + // MARK: - Services + + func observeSoundMode() { + observationToken = manager.observeCurrentMode { [weak self] _ in + self?.updateSoundModeLabel() + } + } + + // MARK: - View + + func setupView() { + updateSoundModeLabel() + updateObservationButton() + } + + func updateSoundModeLabel() { + print(manager.currentMode) + switch manager.currentMode { + case .notDetermined: + soundModeLabel.text = "Not determined" + case .silent: + soundModeLabel.text = "📴 Silent" + case .ring: + soundModeLabel.text = "📳 Ring" + } + } + + func updateObservationButton() { + let title = isObserved ? "End observing" : "Begin observing" + observationButton.setTitle(title, for: .normal) + } + + // MARK: - Actions + + @IBAction func observationButtonDidTap(_ sender: UIButton) { + isObserved.toggle() + isObserved ? manager.beginUpdatingCurrentMode() : manager.endUpdatingCurrentMode() + updateObservationButton() + } + +} diff --git a/SoundModeManagerTests/Source/Managers/SoundModeManagerTests.swift b/SoundModeManagerTests/Source/Managers/SoundModeManagerTests.swift new file mode 100644 index 0000000..61f90b5 --- /dev/null +++ b/SoundModeManagerTests/Source/Managers/SoundModeManagerTests.swift @@ -0,0 +1,49 @@ +// Copyright © 2021 Yurii Lysytsia. All rights reserved. + +import XCTest +@testable import SoundModeManager + +final class SoundModeManagerTests: XCTestCase { + + // MARK: - Private Properties + + private var sut: SoundModeManager! + private var observationToken: SoundModeManager.ObservationToken? + + // MARK: - Lifecycle + + override func setUpWithError() throws { + sut = SoundModeManager() + + // Check default mode before each tests. + XCTAssertEqual(sut.currentMode, .notDetermined) + } + + // MARK: - Tests + + func testUpdateCurrentMode() { + // Update current mode once + let expectation = XCTestExpectation(description: "Update current mode once") + sut.updateCurrentMode { mode in + XCTAssertNotEqual(mode, .notDetermined) + + // Check is block will be called in the second time + self.sut.updateCurrentMode { newMode in + XCTAssertEqual(newMode, mode) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 10) + } + + func testCurrentModeObserver() { + let expectation = XCTestExpectation(description: "Update current mode when it changes") + observationToken = sut.observeCurrentMode { mode in + XCTAssertNotEqual(mode, .notDetermined) + expectation.fulfill() + } + sut.beginUpdatingCurrentMode() + wait(for: [expectation], timeout: 10) + } + +} diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..7be4830 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..89abfd5 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,81 @@ +default_platform(:ios) + +PROJECT_NAME = "SoundModeManager" +PROJECT_FILE = "#{PROJECT_NAME}.xcodeproj" +POSPEC_FILE = "#{PROJECT_NAME}.podspec" + +desc "Update project version and push all changes after" +lane :update_version do |options| + # Get base branch name + branch = options[:git_branch] + + if branch.nil? + branch = git_branch() + UI.important "Default branch name is `#{branch}`" + else + UI.message "Custom branch name is `#{branch}`" + end + + if !branch.start_with?('rc/') + UI.user_error! "Branch version is invalid. Base branch name must have prefix `rc/**`" + end + + # Get branch version + branch_version = branch.split("/").last().strip() + + if branch_version.empty? + UI.user_error! "Branch version is invalid. Please check your branch name. It must have `rc/..` format" + end + + UI.message "Current branch version is `#{branch_version}`" + + # Upgrade Xcode project version number with given branch version. + increment_version_number( + version_number: branch_version, + xcodeproj: PROJECT_FILE + ) + + # Upgrade podspec version number with given branch version. + version_bump_podspec( + path: POSPEC_FILE, + version_number: branch_version + ) + + # Commit all changes. + git_commit( + path: ["*.podspec", "*.xcodeproj/project.pbxproj", "Info.plist"], + message: "Updated project version to `#{branch_version}`", + allow_nothing_to_commit: true + ) + + # Push the commit to the origin. + push_to_git_remote() +end + +desc "Build project and run unit tests" +lane :build_and_test do + run_tests( + project: PROJECT_FILE, + scheme: PROJECT_NAME + ) +end + +desc "Get Xcode project version, add git tag and push changes to trunk" +lane :prepare_to_deploy do + # Prepare + UI.message "Prepare to deploy to git branch `#{git_branch}`" + + # Get current project version number + version_number = get_version_number( + xcodeproj: PROJECT_FILE, + target: PROJECT_NAME + ) + + # Add git tag if not exist and push to remote + if git_tag_exists(tag: version_number, remote: true) + UI.error "Tag `#{version_number}` was already exist" + else + add_git_tag(tag: version_number) + push_git_tags(tag: version_number.shellescape) + end +end \ No newline at end of file diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..8c4a374 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,38 @@ +fastlane documentation +================ +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +``` +xcode-select --install +``` + +Install _fastlane_ using +``` +[sudo] gem install fastlane -NV +``` +or alternatively using `brew install fastlane` + +# Available Actions +### update_version +``` +fastlane update_version +``` +Update project version and push all changes after +### build_and_test +``` +fastlane build_and_test +``` +Build project and run unit tests +### prepare_to_deploy +``` +fastlane prepare_to_deploy +``` +Get Xcode project version, add git tag and push changes to trunk + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. +More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). +The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).