first commit

This commit is contained in:
Artemon4ik8091
2026-07-14 12:06:58 +03:00
commit 01088b112b
31 changed files with 4678 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>music player.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>music playerWatch Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
BIN
View File
Binary file not shown.
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIcon 1.jpg",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
import Foundation
import WatchConnectivity
import Combine
// MARK: - Лёгкая модель трека для передачи на часы
struct WatchTrackInfo: Codable {
let id: String // используем имя файла как стабильный идентификатор
let title: String
let artist: String
}
// MARK: - Связь iPhone <-> Apple Watch
// Добавь этот файл в таргет "music player" (основное приложение).
class PhoneConnectivityManager: NSObject, WCSessionDelegate, ObservableObject {
static let shared = PhoneConnectivityManager()
private var cancellables = Set<AnyCancellable>()
private override init() {
super.init()
activateSession()
// Откладываем обращение к MusicManager.shared на следующий "тик"
// иначе при первом запуске получится циклическая инициализация
// двух синглтонов друг через друга и приложение упадёт.
DispatchQueue.main.async { [weak self] in
self?.observeMusicManager()
}
}
private func activateSession() {
guard WCSession.isSupported() else { return }
let session = WCSession.default
session.delegate = self
session.activate()
}
// MARK: - Отправка состояния плеера на часы
private func observeMusicManager() {
let manager = MusicManager.shared
Publishers.CombineLatest4(
manager.$tracks,
manager.$currentTrackIndex,
manager.$isPlaying,
manager.$currentTitle
)
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
.sink { [weak self] _ in
self?.pushStateToWatch()
}
.store(in: &cancellables)
}
func pushStateToWatch() {
guard WCSession.default.activationState == .activated else { return }
let manager = MusicManager.shared
let trackInfos = manager.tracks.map {
WatchTrackInfo(id: $0.url.lastPathComponent, title: $0.title, artist: $0.artist)
}
guard let tracksData = try? JSONEncoder().encode(trackInfos) else { return }
let context: [String: Any] = [
"tracks": tracksData,
"isPlaying": manager.isPlaying,
"currentTitle": manager.currentTitle,
"currentArtist": manager.currentArtist,
"currentId": manager.currentTrack?.url.lastPathComponent ?? ""
]
try? WCSession.default.updateApplicationContext(context)
}
// MARK: - WCSessionDelegate
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
DispatchQueue.main.async { self.pushStateToWatch() }
}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) { WCSession.default.activate() }
// Команды, пришедшие пока приложение на телефоне активно
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
handleCommand(message)
}
// Команды, пришедшие "в очереди" (если телефон был недоступен)
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
handleCommand(userInfo)
}
// MARK: - Обработка команд с часов
private func handleCommand(_ command: [String: Any]) {
guard let action = command["action"] as? String else { return }
DispatchQueue.main.async {
let manager = MusicManager.shared
switch action {
case "playPause":
manager.playPause()
case "next":
manager.nextTrack()
case "previous":
manager.prevTrack()
case "playTrack":
if let id = command["id"] as? String,
let index = manager.currentList.firstIndex(where: { $0.url.lastPathComponent == id }) {
manager.playTrack(at: index)
}
default:
break
}
self.pushStateToWatch()
}
}
}
Binary file not shown.
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+17
View File
@@ -0,0 +1,17 @@
//
// music_playerApp.swift
// music player
//
// Created by aswer on 23.06.2026.
//
import SwiftUI
@main
struct music_playerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
+36
View File
@@ -0,0 +1,36 @@
//
// music_playerTests.swift
// music playerTests
//
// Created by aswer on 23.06.2026.
//
import XCTest
@testable import music_player
final class music_playerTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
@@ -0,0 +1,41 @@
//
// music_playerUITests.swift
// music playerUITests
//
// Created by aswer on 23.06.2026.
//
import XCTest
final class music_playerUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}
@@ -0,0 +1,32 @@
//
// music_playerUITestsLaunchTests.swift
// music playerUITests
//
// Created by aswer on 23.06.2026.
//
import XCTest
final class music_playerUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,62 @@
//
// ContentView.swift
// music playerWatch Watch App
//
// Created by aswer on 30.06.2026.
//
import SwiftUI
// MARK: - Главный экран часов
// Замени этим содержимым ContentView.swift, который Xcode сгенерировал
// в таргете часов (структура должна остаться по имени ContentView,
// чтобы App.swift на часах продолжил работать без изменений).
struct ContentView: View {
@StateObject private var connectivity = WatchConnectivityManager.shared
var body: some View {
NavigationView {
List {
if !connectivity.currentTitle.isEmpty {
NavigationLink(destination: NowPlayingView()) {
HStack {
Image(systemName: connectivity.isPlaying ? "pause.circle.fill" : "play.circle.fill")
.foregroundColor(.green)
VStack(alignment: .leading) {
Text(connectivity.currentTitle)
.font(.headline)
.lineLimit(1)
Text(connectivity.currentArtist)
.font(.caption)
.foregroundColor(.gray)
.lineLimit(1)
}
}
}
}
if connectivity.tracks.isEmpty {
Text("Открой приложение на iPhone, чтобы список треков появился здесь")
.font(.caption2)
.foregroundColor(.gray)
} else {
ForEach(connectivity.tracks) { track in
Button(action: { connectivity.playTrack(id: track.id) }) {
VStack(alignment: .leading) {
Text(track.title)
.font(.body)
.foregroundColor(track.id == connectivity.currentId ? .green : .primary)
.lineLimit(1)
Text(track.artist)
.font(.caption2)
.foregroundColor(.gray)
.lineLimit(1)
}
}
}
}
}
.navigationTitle("Музыка")
}
}
}
@@ -0,0 +1,41 @@
import SwiftUI
// MARK: - Экран "Сейчас играет" на часах
struct NowPlayingView: View {
@StateObject private var connectivity = WatchConnectivityManager.shared
var body: some View {
VStack(spacing: 10) {
Text(connectivity.currentTitle)
.font(.headline)
.multilineTextAlignment(.center)
.lineLimit(2)
Text(connectivity.currentArtist)
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(1)
HStack(spacing: 22) {
Button(action: connectivity.previous) {
Image(systemName: "backward.fill")
.font(.title2)
}
Button(action: connectivity.playPause) {
Image(systemName: connectivity.isPlaying ? "pause.fill" : "play.fill")
.font(.largeTitle)
}
Button(action: connectivity.next) {
Image(systemName: "forward.fill")
.font(.title2)
}
}
.buttonStyle(.plain)
.padding(.top, 6)
}
.padding()
.navigationTitle("Сейчас играет")
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,73 @@
import Foundation
import WatchConnectivity
import Combine
// MARK: - Та же лёгкая модель трека, что и на телефоне
struct WatchTrackInfo: Codable, Identifiable {
let id: String
let title: String
let artist: String
}
// MARK: - Связь Apple Watch <-> iPhone
// Добавь этот файл в таргет часов (например "music playerWatch").
class WatchConnectivityManager: NSObject, WCSessionDelegate, ObservableObject {
static let shared = WatchConnectivityManager()
@Published var tracks: [WatchTrackInfo] = []
@Published var isPlaying: Bool = false
@Published var currentTitle: String = ""
@Published var currentArtist: String = ""
@Published var currentId: String = ""
private override init() {
super.init()
guard WCSession.isSupported() else { return }
let session = WCSession.default
session.delegate = self
session.activate()
}
// MARK: - WCSessionDelegate
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
DispatchQueue.main.async {
self.apply(applicationContext)
}
}
private func apply(_ context: [String: Any]) {
if let data = context["tracks"] as? Data,
let decoded = try? JSONDecoder().decode([WatchTrackInfo].self, from: data) {
self.tracks = decoded
}
self.isPlaying = context["isPlaying"] as? Bool ?? false
self.currentTitle = context["currentTitle"] as? String ?? ""
self.currentArtist = context["currentArtist"] as? String ?? ""
self.currentId = context["currentId"] as? String ?? ""
}
// MARK: - Отправка команд на телефон
private func send(action: String, extra: [String: Any] = [:]) {
guard WCSession.isSupported() else { return }
var payload = extra
payload["action"] = action
let session = WCSession.default
if session.isReachable {
session.sendMessage(payload, replyHandler: nil) { _ in
session.transferUserInfo(payload)
}
} else {
session.transferUserInfo(payload)
}
}
func playPause() { send(action: "playPause") }
func next() { send(action: "next") }
func previous() { send(action: "previous") }
func playTrack(id: String) { send(action: "playTrack", extra: ["id": id]) }
}
@@ -0,0 +1,17 @@
//
// music_playerWatchApp.swift
// music playerWatch Watch App
//
// Created by aswer on 30.06.2026.
//
import SwiftUI
@main
struct music_playerWatch_Watch_AppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@@ -0,0 +1,36 @@
//
// music_playerWatch_Watch_AppTests.swift
// music playerWatch Watch AppTests
//
// Created by aswer on 30.06.2026.
//
import XCTest
@testable import music_playerWatch_Watch_App
final class music_playerWatch_Watch_AppTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Tests marked async will run the test method on an arbitrary thread managed by the Swift runtime.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
@@ -0,0 +1,41 @@
//
// music_playerWatch_Watch_AppUITests.swift
// music playerWatch Watch AppUITests
//
// Created by aswer on 30.06.2026.
//
import XCTest
final class music_playerWatch_Watch_AppUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}
@@ -0,0 +1,32 @@
//
// music_playerWatch_Watch_AppUITestsLaunchTests.swift
// music playerWatch Watch AppUITests
//
// Created by aswer on 30.06.2026.
//
import XCTest
final class music_playerWatch_Watch_AppUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>