Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/build-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: 🧩 Build Package

on:
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: macos-latest

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Check last commit for skip keyword
run: python workflow_scripts/check_latest_commit_for_skip.py >> $GITHUB_ENV

- name: ⏩ SKIPPING REMAINING STEPS 👀
if: env.should_skip == 'true'
run: exit 0

- name: Setup Swift
if: env.should_skip == 'false'
uses: swift-actions/[email protected]
with:
swift-version: '5.10' # Should match swift-tools-version in Package.swift

- name: Build Control Library
if: env.should_skip == 'false'
run: |
xcodebuild -scheme Control \
-sdk iphonesimulator \
-configuration Release \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
BUILD_DIR=$(pwd)/build/Control

- name: Build Controllers Library
if: env.should_skip == 'false'
run: |
xcodebuild -scheme Controllers \
-sdk iphonesimulator \
-configuration Release \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
BUILD_DIR=$(pwd)/build/Controllers
44 changes: 44 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: 🧹 Lint

on:
pull_request:
branches: [ "main" ]
types: [opened, synchronize]

# push:
# Every push if left empty

jobs:

lint-code:
runs-on: macos-latest

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Check last commit for skip keyword
run: python workflow_scripts/check_latest_commit_for_skip.py >> $GITHUB_ENV

- name: ⏩ SKIPPING REMAINING STEPS 👀
if: env.should_skip == 'true'
run: exit 0

- name: Set up Ruby
if: env.should_skip == 'false'
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.5' # latest stable as of 2 November 2024

- name: Install SwiftLint
if: env.should_skip == 'false'
run: brew install swiftlint

- name: Lint code using SwiftLint
if: env.should_skip == 'false'
run: swiftlint --strict
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ xcuserdata/
*.xcuserdatad
*.xcbkptlist
*.xcsettings
*.xcscheme
*.xcworkspacedata
*.DS_Store

*SpotifyConfig.swift
*.build
71 changes: 71 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
included:
- Sources

line_length:
warning: 110
error: 150
ignores_comments: true

disabled_rules:
- trailing_whitespace
- trailing_comma
- void_function_in_ternary

opt_in_rules:
- array_init
- attributes
- closure_end_indentation
- closure_spacing
- collection_alignment
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- discouraged_object_literal
- empty_count
- empty_string
- empty_xctest_method
- explicit_init
- fallthrough
- file_header
- file_name
- first_where
- flatmap_over_map_reduce
- identical_operands
- joined_default_parameter
- legacy_random
- let_var_whitespace
- last_where
- literal_expression_end_indentation
- lower_acl_than_parent
- modifier_order
- nimble_operator
- nslocalizedstring_key
- number_separator
- object_literal
- operator_usage_whitespace
- overridden_super_call
- override_in_extension
- pattern_matching_keywords
- private_action
- private_outlet
- prohibited_interface_builder
- prohibited_super_call
- quick_discouraged_call
- quick_discouraged_focused_test
- quick_discouraged_pending_test
- reduce_into
- redundant_nil_coalescing
- redundant_type_annotation
- single_test_class
- sorted_first_last
- sorted_imports
- static_operator
- strong_iboutlet
- toggle_bool
- unavailable_function
- unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- untyped_error_in_catch
- vertical_parameter_alignment_on_call
- xct_specific_matcher
- yoda_condition
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright 2024 Ryan Forsyth

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 changes: 19 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
// swift-tools-version: 6.0
// swift-tools-version: 5.10.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ControlKit",
platforms: [.iOS(.v15)],
products: [
.library(
name: "ControlKit",
targets: ["ControlKit"]),
name: "Controllers",
targets: ["Controllers"]),
.library(
name: "Control",
targets: ["Control"]),
],
dependencies: [
.package(url: "https://github.com/spotify/ios-sdk.git", from: "3.0.0")
],
targets: [
.target(
name: "ControlKit"),

name: "Controllers",
dependencies: [
"Control",
.product(name: "SpotifyiOS", package: "ios-sdk")
]
),
.target(
name: "Control"
)
]
)
128 changes: 128 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
![ControlKit Light](https://github.com/user-attachments/assets/11ad00d7-a200-46bc-bc99-4b214131dfe7#gh-light-mode-only)
![ControlKit Dark](https://github.com/user-attachments/assets/b0d1fa79-4b56-4fd1-bb5b-4a25c9e3254c#gh-dark-mode-only)

# ControlKit

**ControlKit** is a minimal Swift Package enabling control of media playback and system volume.

![Minimum iOS Version](https://img.shields.io/badge/%F0%9F%93%B1%20iOS-15%2B-blue.svg) ![Build Status](https://github.com/superturboryan/ControlKit/workflows/%F0%9F%A7%A9%20Build%20Package/badge.svg) ![Lint](https://github.com/superturboryan/ControlKit/workflows/%F0%9F%A7%B9%20Lint/badge.svg) ![Contributors](https://img.shields.io/github/contributors/superturboryan/ci-playground)

### TLDR

Control the device's volume with one line:

```swift
Control.Volume.increase() // 🔊 🆙
```

## Installation

Add ControlKit as a dependency in your Xcode project:

1. Go to **File > Add Package Dependencies…**

2. Enter the package URL in the search bar:

```
https://github.com/superturboryan/ControlKit.git
```

3. Choose the libraries you want to include:

![Screenshot 2024-10-28 at 17 46 50](https://github.com/user-attachments/assets/48e0a678-fd75-4056-9754-867a11b87d67)

## Requirements

[`SpotifyController`](Sources/Controllers/SpotifyController.swift) requires a Spotify _Client ID_
and _Redirect URL_ to authorize with & control the Spotify app.

1. [Define a custom URL scheme for your app](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app).
Add a `URL Type` to the target's `Info.plist`.

2. Create an app in the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard)
to get a _client ID_ and register your _redirect URL_ (scheme).

<img width="1007" alt="Screenshot 2024-10-29 at 17 31 24" src="https://github.com/user-attachments/assets/895c8092-4a9d-4526-9f85-5da7b868fbc1">

### Warning 👇

The Spotify access token is **not persisted across app launches** by default.

You must provide an object conforming to **`DAO<String>`** if you want the access token to be persisted.

## Usage

### Control

```swift
import Control

// 🔊 Decrement system volume
Control.Volume.decreaseVolume()

// 🕵️‍♂️ Check if audio is being played (by another app)
if Control.Playback.isAudioPlaying {
// 💃🕺
}

// ⏭️ Skip to next track (Apple Music only - use Controllers.SpotifyController for Spotify 💚)
Control.Playback.AppleMusic.skipToNextTrack()

// 🫨 Vibrate
Control.Haptics.vibrate()
```

### Controllers

```swift
// App.swift

import Controllers
import SwiftUI

@main struct YourApp: App {

@StateObject var spotify = SpotifyController(
config: .init(
clientID: Secrets.clientID,
redirectURL: "controlkit://spotify"
)
)

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(spotify)
.onOpenURL { spotify.setAccessToken(from: $0) } // Parse access token from URL
}
}

func skipToNextSpotifyTrack() {
spotify.skipToNextTrack()
}
}

// Secrets.swift 🔐
// Don't forget to gitignore this 🙈

enum Secrets {
static let clientID = "<your client id>"
}
```

## Dependencies

📚 [AVFAudio](https://developer.apple.com/documentation/avfaudio)
📚 [Media Player](https://developer.apple.com/documentation/mediaplayer/)
📦 [SpotifyiOS](https://github.com/spotify/ios-sdk)

## Contributing

Contributions and feedback are welcome! 🧑‍💻👩‍💻

Here are a few guidelines:

- You can [open an Issue](https://github.com/superturboryan/ControlKit/issues/new) or raise a PR 🤝
- Commit messages should contain emojis ❤️ and be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) 🔏
- [Workflows](https://github.com/superturboryan/ControlKit/actions) should be green 🟢
- `main` should be [linear](https://stackoverflow.com/questions/20348629/what-are-the-advantages-of-keeping-linear-history-in-git) 🎋
12 changes: 12 additions & 0 deletions Sources/Control/Control.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Control.swift
// ControlKit
//

/// Library namespace.
public enum Control {}

extension Control {

static let subsystem = "com.ControlKit.Control"
}
20 changes: 20 additions & 0 deletions Sources/Control/Extensions/BetweenZeroAndOneInclusive.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// BetweenZeroAndOneInclusive.swift
// ControlKit
//

@propertyWrapper
struct BetweenZeroAndOneInclusive {

private var value: Float
private let range: ClosedRange<Float> = 0.0...1.0

init(wrappedValue: Float) {
value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}

var wrappedValue: Float {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
Loading