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
Binary file removed .DS_Store
Binary file not shown.
20 changes: 9 additions & 11 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
name: "Camera Button CI"

on:
on:
push:
branches:
- master
pull_request:

jobs:
iOS:
name: Test iOS
runs-on: macOS-latest
env:
DEVELOPER_DIR: /Applications/Xcode_11.3.app/Contents/Developer
strategy:
matrix:
destination: ["OS=13.1,name=iPhone 11 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"]
name: Build iOS
runs-on: macos-15
steps:
- uses: actions/checkout@v1
- name: iOS - ${{ matrix.destination }}
run: xcodebuild -project "CameraButton.xcodeproj" -destination "${{ matrix.destination }}"
- uses: actions/checkout@v4
- name: Build library
run: xcodebuild build -scheme CameraButton -destination 'generic/platform=iOS Simulator'
- name: Build example
run: xcodebuild build -project CameraButton.xcodeproj -scheme CameraButton-Example -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.DS_Store
xcuserdata/
.build/
.swiftpm/
DerivedData/
35 changes: 35 additions & 0 deletions CameraButton-Example/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@
</segue>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="gL1-aB-c01" style="IBUITableViewCellStyleDefault" id="gL1-aB-c02">
<rect key="frame" x="0.0" y="115" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="gL1-aB-c02" id="gL1-aB-c03">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Liquid Glass" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="gL1-aB-c01">
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="gL1-aB-v01" kind="show" id="gL1-aB-s01"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
Expand Down Expand Up @@ -155,6 +175,21 @@
</objects>
<point key="canvasLocation" x="1954" y="824"/>
</scene>
<!--Glass View Controller-->
<scene sceneID="gL1-aB-x01">
<objects>
<viewController id="gL1-aB-v01" customClass="GlassViewController" customModule="CameraButton_Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="gL1-aB-w01">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="gL1-aB-g01"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="gL1-aB-f01" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1954" y="1624"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="kgp-rK-xh2">
<objects>
Expand Down
164 changes: 164 additions & 0 deletions CameraButton-Example/GlassViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// GlassViewController.swift
// CameraButton-Example
//
// Copyright © 2026 Ramiz Kichibekov. All rights reserved.
//

import UIKit
import SwiftUI
import CameraButton

final class GlassViewController: UIViewController {

// MARK: Properties

private let maxRecordingDuration: TimeInterval = 10

private lazy var cameraButton: CameraButton = {
let button = CameraButton()
button.style = .glass
button.delegate = self
button.translatesAutoresizingMaskIntoConstraints = false

return button
}()

private lazy var segmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: ["Camera", "Video"])
control.selectedSegmentIndex = .zero
control.addAction(UIAction { [unowned self] _ in
self.cameraButton.type = CameraType(withValue: control.selectedSegmentIndex)
}, for: .valueChanged)

return control
}()

// MARK: Life cycle

override func viewDidLoad() {
super.viewDidLoad()

navigationItem.titleView = segmentedControl
configureBackground()
configureLayout()
}

// MARK: Private methods

private func configureBackground() {
let gradient = CAGradientLayer()
gradient.colors = [UIColor.systemPink, .systemOrange, .systemTeal].map(\.cgColor)
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
let background = GradientView(gradient: gradient)
background.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(background)
NSLayoutConstraint.activate([
background.topAnchor.constraint(equalTo: view.topAnchor),
background.bottomAnchor.constraint(equalTo: view.bottomAnchor),
background.leadingAnchor.constraint(equalTo: view.leadingAnchor),
background.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}

private func configureLayout() {
let uiKitLabel = makeCaption("UIKit · CameraButton")
let swiftUILabel = makeCaption("SwiftUI · CameraShutterButton")
let hosting = UIHostingController(rootView: SwiftUIDemo())
hosting.view.backgroundColor = .clear
hosting.view.translatesAutoresizingMaskIntoConstraints = false
addChild(hosting)

let stack = UIStackView(arrangedSubviews: [uiKitLabel, cameraButton, swiftUILabel, hosting.view])
stack.axis = .vertical
stack.alignment = .center
stack.spacing = 16
stack.setCustomSpacing(56, after: cameraButton)
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
hosting.didMove(toParent: self)

NSLayoutConstraint.activate([
cameraButton.widthAnchor.constraint(equalToConstant: 76),
cameraButton.heightAnchor.constraint(equalToConstant: 76),
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}

private func makeCaption(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .preferredFont(forTextStyle: .footnote)
label.textColor = .white

return label
}

}

// MARK: - CameraButtonDelegate

extension GlassViewController: CameraButtonDelegate {

func didTapButton(_ button: CameraButton) {
guard button.type == .video, button.isSelected else { return }

button.animateRecordingProgress(duration: maxRecordingDuration)
}

}

// MARK: - SwiftUI demo

private struct SwiftUIDemo: View {

@State private var isRecording = false
@State private var takenPhotos = 0

var body: some View {
HStack(spacing: 40) {
VStack(spacing: 8) {
CameraShutterButton(type: .photo, style: .glass) {
takenPhotos += 1
}
.frame(width: 64, height: 64)
Text("Photos: \(takenPhotos)")
}
VStack(spacing: 8) {
CameraShutterButton(type: .video, style: .glass, isRecording: $isRecording)
.frame(width: 64, height: 64)
Text(isRecording ? "Recording" : "Idle")
}
}
.font(.footnote)
.foregroundStyle(.white)
}

}

// MARK: - GradientView

private final class GradientView: UIView {

private let gradient: CAGradientLayer

init(gradient: CAGradientLayer) {
self.gradient = gradient
super.init(frame: .zero)

layer.addSublayer(gradient)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
super.layoutSubviews()

gradient.frame = bounds
}

}
16 changes: 16 additions & 0 deletions CameraButton.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
4B8D4C8B2D2BFD0200751CC3 /* CameraType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8D4C862D2BFD0200751CC3 /* CameraType.swift */; };
4B8D4C8C2D2BFD0200751CC3 /* CameraButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B8D4C842D2BFD0200751CC3 /* CameraButton.h */; };
4BA40D722D2C005D007466AD /* CameraButtonDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA40D712D2C0057007466AD /* CameraButtonDelegate.swift */; };
4BF1A0032E9A000000C0FFEE /* CameraShutterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF1A0022E9A000000C0FFEE /* CameraShutterButton.swift */; };
4BF1A0052E9A000000C0FFEE /* GlassViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF1A0042E9A000000C0FFEE /* GlassViewController.swift */; };
4BC6F2EF257F9D3000691CE9 /* CustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6F2EE257F9D3000691CE9 /* CustomViewController.swift */; };
4BE099C5233D339B00AE7670 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE099C4233D339B00AE7670 /* AppDelegate.swift */; };
4BE099C7233D339B00AE7670 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE099C6233D339B00AE7670 /* SceneDelegate.swift */; };
Expand Down Expand Up @@ -52,6 +54,8 @@
4B8D4C862D2BFD0200751CC3 /* CameraType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraType.swift; sourceTree = "<group>"; };
4B8D4C872D2BFD0200751CC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4BA40D712D2C0057007466AD /* CameraButtonDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraButtonDelegate.swift; sourceTree = "<group>"; };
4BF1A0022E9A000000C0FFEE /* CameraShutterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraShutterButton.swift; sourceTree = "<group>"; };
4BF1A0042E9A000000C0FFEE /* GlassViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassViewController.swift; sourceTree = "<group>"; };
4BC6F2EE257F9D3000691CE9 /* CustomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = "<group>"; };
4BE099B1233D331200AE7670 /* CameraButton.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CameraButton.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4BE099C2233D339B00AE7670 /* CameraButton-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CameraButton-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -87,6 +91,7 @@
isa = PBXGroup;
children = (
4BA40D702D2C0050007466AD /* Protocols */,
4BF1A0012E9A000000C0FFEE /* SwiftUI */,
4B8D4C842D2BFD0200751CC3 /* CameraButton.h */,
4B8D4C852D2BFD0200751CC3 /* CameraButton.swift */,
4B8D4C862D2BFD0200751CC3 /* CameraType.swift */,
Expand All @@ -103,6 +108,14 @@
path = Protocols;
sourceTree = "<group>";
};
4BF1A0012E9A000000C0FFEE /* SwiftUI */ = {
isa = PBXGroup;
children = (
4BF1A0022E9A000000C0FFEE /* CameraShutterButton.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
};
4BE099A7233D331200AE7670 = {
isa = PBXGroup;
children = (
Expand All @@ -129,6 +142,7 @@
4BE099C6233D339B00AE7670 /* SceneDelegate.swift */,
4BE099C8233D339B00AE7670 /* BasicViewController.swift */,
4BC6F2EE257F9D3000691CE9 /* CustomViewController.swift */,
4BF1A0042E9A000000C0FFEE /* GlassViewController.swift */,
4BE099CA233D339B00AE7670 /* Main.storyboard */,
4BE099CD233D339D00AE7670 /* Assets.xcassets */,
4BE099CF233D339D00AE7670 /* LaunchScreen.storyboard */,
Expand Down Expand Up @@ -262,6 +276,7 @@
files = (
4B8D4C8A2D2BFD0200751CC3 /* CameraButton.swift in Sources */,
4BA40D722D2C005D007466AD /* CameraButtonDelegate.swift in Sources */,
4BF1A0032E9A000000C0FFEE /* CameraShutterButton.swift in Sources */,
4B8D4C8B2D2BFD0200751CC3 /* CameraType.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -274,6 +289,7 @@
4BE099C5233D339B00AE7670 /* AppDelegate.swift in Sources */,
4BE099C7233D339B00AE7670 /* SceneDelegate.swift in Sources */,
4BC6F2EF257F9D3000691CE9 /* CustomViewController.swift in Sources */,
4BF1A0052E9A000000C0FFEE /* GlassViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "CameraButton",
platforms: [
.iOS(.v14)
.iOS(.v15)
],
products: [
.library(
Expand Down
Loading
Loading