Skip to content

Commit

Permalink
[#157] HapticService 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
00yhsp committed Nov 20, 2024
1 parent b234f2e commit 23c1c8b
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 26 deletions.
4 changes: 4 additions & 0 deletions AGAMI/Sources/Presentation/View/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct HomeView: View {
.foregroundStyle(Color(.pWhite), Color(.pGray3))
.onTapGesture {
coordinator.push(route: .accountView)
viewModel.simpleHaptic()
}

Capsule()
Expand All @@ -54,6 +55,7 @@ struct HomeView: View {
withAnimation {
viewModel.selectedTab = .plake
}
viewModel.simpleHaptic()
}
Spacer()

Expand All @@ -65,6 +67,7 @@ struct HomeView: View {
withAnimation {
viewModel.selectedTab = .map
}
viewModel.simpleHaptic()
}
}
.padding(EdgeInsets(top: 0, leading: 29.25, bottom: 0, trailing: 32.25))
Expand All @@ -78,6 +81,7 @@ struct HomeView: View {
.foregroundStyle(Color(.pWhite), Color(.pPrimary))
.onTapGesture {
coordinator.push(route: .newPlakeView)
viewModel.simpleHaptic()
}

Spacer()
Expand Down
5 changes: 4 additions & 1 deletion AGAMI/Sources/Presentation/View/Plake/PlakeListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ private struct SearchBar: View {
}
}
}
.onChange(of: isFocused) {
viewModel.simpleHaptic()
}
}
}

Expand All @@ -109,7 +112,7 @@ private struct ListView: View {
Group {
if listCellPlaceholder.showArchiveListUpLoadingCell {
ArchiveListUpLoadingCell(viewModel: viewModel, size: size)
} else if viewModel.playlists.isEmpty && !viewModel.isFetching {
} else if viewModel.isShowingNewPlake {
MakeNewPlakeCell(size: size)
}
ForEach(viewModel.playlists, id: \.playlistID) { playlist in
Expand Down
41 changes: 28 additions & 13 deletions AGAMI/Sources/Presentation/View/Plake/PlakePlaylistView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ struct PlakePlaylistView: View {
@State var viewModel: PlakePlaylistViewModel
@Environment(\.scenePhase) private var scenePhase
@Environment(\.openURL) private var openURL
@Environment(PlakeCoordinator.self) private var coordinator

init(viewModel: PlakePlaylistViewModel) {
_viewModel = State(wrappedValue: viewModel)
Expand All @@ -41,21 +40,11 @@ struct PlakePlaylistView: View {
.refreshable { viewModel.refreshPlaylist() }
.background(Color(.pLightGray))
.toolbar {
if !viewModel.presentationState.isEditing {
ToolbarItem(placement: .topBarLeading) {
Button {
coordinator.pop()
} label: {
Image(systemName: "chevron.backward")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(.pPrimary))
}
}
ToolbarItem(placement: .topBarLeading) {
TopBarLeadingItems(viewModel: viewModel)
}

ToolbarItem(placement: .topBarTrailing) {
TopBarTrailingItems(viewModel: viewModel)
.foregroundStyle(Color(.pPrimary))
}
}
.navigationTitle(viewModel.presentationState.isEditing ? "편집하기" : "")
Expand Down Expand Up @@ -452,13 +441,32 @@ private struct PhotoConfirmationDialogActions: View {
}
}

private struct TopBarLeadingItems: View {
@Environment(PlakeCoordinator.self) private var coordinator
let viewModel: PlakePlaylistViewModel

var body: some View {
if !viewModel.presentationState.isEditing {
Button {
viewModel.simpleHaptic()
coordinator.pop()
} label: {
Image(systemName: "chevron.backward")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(.pPrimary))
}
}
}
}

private struct TopBarTrailingItems: View {
let viewModel: PlakePlaylistViewModel

var body: some View {
HStack {
if viewModel.presentationState.isEditing {
Button {
viewModel.simpleHaptic()
viewModel.resetPlaylist()
viewModel.presentationState.isEditing = false
} label: {
Expand All @@ -468,6 +476,7 @@ private struct TopBarTrailingItems: View {
}

Button(role: .cancel) {
viewModel.simpleHaptic()
Task {
await viewModel.applyChangesToFirestore()
viewModel.presentationState.isEditing = false
Expand All @@ -484,6 +493,7 @@ private struct TopBarTrailingItems: View {
.disabled(viewModel.presentationState.isUpdating)
} else if viewModel.exportingState == .none {
Button {
viewModel.simpleHaptic()
viewModel.presentationState.isEditing = true
} label: {
Text("편집")
Expand All @@ -499,6 +509,7 @@ private struct TopBarTrailingItems: View {
}
}
}
.foregroundStyle(Color(.pPrimary))
}
}

Expand All @@ -509,6 +520,7 @@ private struct MenuContents: View {

var body: some View {
Button {
viewModel.simpleHaptic()
coordinator.push(route: .addPlakingView(
viewModel: AddPlakingViewModel(playlist: viewModel.playlist)
))
Expand All @@ -517,6 +529,7 @@ private struct MenuContents: View {
}

Button {
viewModel.simpleHaptic()
Task {
if let url = await viewModel.getInstagramStoryURL() {
openURL(url)
Expand All @@ -527,6 +540,7 @@ private struct MenuContents: View {
}

Button {
viewModel.simpleHaptic()
Task {
await viewModel.downloadPhotoAndSaveToAlbum()
}
Expand All @@ -535,6 +549,7 @@ private struct MenuContents: View {
}

Button(role: .destructive) {
viewModel.simpleHaptic()
viewModel.presentationState.isShowingDeletePlakeAlert = true
} label: {
Label("삭제하기", systemImage: "trash")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ struct SearchShazamingView: View {
}
.onAppearAndActiveCheckUserValued(scenePhase)
.navigationBarBackButtonHidden()
.onAppear {
viewModel.startRecognition()
}
.onAppear(perform: viewModel.startRecognition)
.onDisappear(perform: viewModel.stopRecognition)
.onChange(of: viewModel.shazamStatus) { _, newStatus in
if newStatus == .found {
coordinator.pop()
Expand Down
4 changes: 4 additions & 0 deletions AGAMI/Sources/Presentation/ViewModel/Home/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ import Foundation
@Observable
final class HomeViewModel {
var selectedTab: TabSelection = .plake

func simpleHaptic() {
HapticService.shared.playSimpleHaptic()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ final class PlakeListViewModel {
private let musicService = MusicService()

var isFetching: Bool = false
var isSearching: Bool {
!searchText.isEmpty
}
var isShowingNewPlake: Bool {
playlists.isEmpty && !isFetching && !isSearching
}

var playlists: [PlaylistModel] = []
private var unfilteredPlaylists: [PlaylistModel] = []
var isUploading: Bool = false
Expand All @@ -38,10 +45,8 @@ final class PlakeListViewModel {
}

Task {
defer {
isFetching = false
}

defer { isFetching = false }

if let playlistModels = try? await firebaseService.fetchPlaylistsByUserID(userID: uid) {
await updatePlaylists(sortPlaylistsByDate(playlistModels))
} else {
Expand Down Expand Up @@ -147,7 +152,11 @@ final class PlakeListViewModel {
exportingState = .none
}
}


func simpleHaptic() {
HapticService.shared.playSimpleHaptic()
}

// MARK: - FirebaseListner 코드
// private let listenerService = FirebaseListenerService()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,8 @@ final class PlakePlaylistViewModel: Hashable {
}
return nil
}

func simpleHaptic() {
HapticService.shared.playSimpleHaptic()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ extension SearchShazamingViewModel: ShazamServiceDelegate {

if let item = currentItem {
do {
HapticService.shared.playLongHaptic()
try persistenceService.saveSongToDiggingList(from: item)
} catch {
dump("Failed to save song: \(error)")
Expand All @@ -84,11 +85,13 @@ extension SearchShazamingViewModel: ShazamServiceDelegate {
}

func shazamService(_ service: ShazamService, didNotFindMatchFor signature: SHSignature, error: (any Error)?) {
HapticService.shared.playLongHaptic()
shazamStatus = .failed
stopRecognition()
}

func shazamService(_ service: ShazamService, didFailWithError error: any Error) {
HapticService.shared.playLongHaptic()
shazamStatus = .failed
stopRecognition()
}
Expand Down
83 changes: 83 additions & 0 deletions AGAMI/Sources/Service/Haptic/HapticService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// HapticService.swift
// AGAMI
//
// Created by 박현수 on 11/17/24.
//

import CoreHaptics
import Foundation

final class HapticService {
private var hapticEngine: CHHapticEngine?
private var isSupported: Bool = false

static let shared = HapticService()

private let simpleHapticEvent = [
CHHapticEvent(
eventType: .hapticTransient,
parameters: [],
relativeTime: 0
)
]

private let longHapticEvent = [
CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.0
),
CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.1,
duration: 0.5
)
]

private init() {
setupHapticEngine()
}

private func setupHapticEngine() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
dump("Haptics not supported on this device.")
return
}
isSupported = true

do {
hapticEngine = try CHHapticEngine()
hapticEngine?.isAutoShutdownEnabled = true
} catch {
dump("Failed to create haptic engine: \(error)")
}
}

func playSimpleHaptic() {
guard isSupported,
let pattern = try? CHHapticPattern(events: simpleHapticEvent, parameters: []),
let player = try? hapticEngine?.makePlayer(with: pattern)
else { return }

try? hapticEngine?.start()
try? player.start(atTime: CHHapticTimeImmediate)
}

func playLongHaptic() {
guard isSupported,
let pattern = try? CHHapticPattern(events: longHapticEvent, parameters: []),
let player = try? hapticEngine?.makePlayer(with: pattern)
else { return }

try? hapticEngine?.start()
try? player.start(atTime: CHHapticTimeImmediate)
}
}
8 changes: 4 additions & 4 deletions AGAMI/Sources/Service/Persistence/PersistenceService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import ShazamKit
import SwiftData

final class PersistenceService {
let modelContainer: ModelContainer
let modelContext: ModelContext

static let shared: PersistenceService = .init()
private let modelContainer: ModelContainer
private let modelContext: ModelContext

private let diggingListOrderKey = "diggingListOrder"

private var model: SwiftDataPlaylistModel?

static let shared: PersistenceService = .init()

private init() {
let schema = Schema([SwiftDataPlaylistModel.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
Expand Down

0 comments on commit 23c1c8b

Please sign in to comment.