diff --git a/Namo_SwiftUI/Projects/Feature/Home/Sources/RecordEdit/ImageView/HomeEntireImageStore.swift b/Namo_SwiftUI/Projects/Feature/Home/Sources/RecordEdit/ImageView/HomeEntireImageStore.swift new file mode 100644 index 00000000..598a9847 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Home/Sources/RecordEdit/ImageView/HomeEntireImageStore.swift @@ -0,0 +1,99 @@ +// +// HomeEntireImageStore.swift +// FeatureHome +// +// Created by 박민서 on 12/11/24. +// + +import ComposableArchitecture +import UIKit +import SharedUtil + +@Reducer +public struct HomeEntireImageStore { + + public init() {} + + @ObservableState + public struct State: Equatable { + public init(imgDataList: [Data], currentPage: Int = 0) { + self.imgDataList = IdentifiedArray(uniqueElements: imgDataList.map { IdentifiableData(data: $0) }) + self.currentPage = currentPage + } + + let imgDataList: IdentifiedArrayOf + var currentPage: Int + var curretImgData: IdentifiableData? { imgDataList[currentPage] } + + // 토스트 + var showToast: Bool = false + var toast: Toast = .none + } + + public enum Action: BindableAction { + case binding(BindingAction) + case tapBackButton + case tapDownloadButton + case downloadImage(imgData: Data) + case pageChanged(page: Int) + case showToast(Toast) + } + + public var body: some ReducerOf { + BindingReducer() + + Reduce { state, action in + switch action { + + case .binding: + return .none + + case .tapBackButton: + print("dismiss") + return .none + case .tapDownloadButton: + let imgData = state.imgDataList[state.currentPage] + return .send(.downloadImage(imgData: imgData.data)) + + case .downloadImage(let imgData): + return .run { send in + do { + guard let image = UIImage(data: imgData) else { throw NSError(domain: "imgData convert failed", code: 1) } + try await saveImageToPhotoLibrary(image) + await send(.showToast(.saveSuccess)) + } catch { + await send(.showToast(.saveFailed)) + } + } + + case let .pageChanged(newPage): + state.currentPage = newPage + return .none + + case .showToast(let toast): + state.toast = toast + state.showToast = true + return .none + } + } + } +} + +extension HomeEntireImageStore { + public enum Toast { + case saveSuccess + case saveFailed + case none + + var content: String { + switch self { + case .saveSuccess: + return "이미지가 저장되었습니다." + case .saveFailed: + return "이미지 저장에 실패했습니다.\n다시 시도해주세요." + case .none: + return "" + } + } + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Home/Sources/RecordEdit/ImageView/HomeEntireImageView.swift b/Namo_SwiftUI/Projects/Feature/Home/Sources/RecordEdit/ImageView/HomeEntireImageView.swift new file mode 100644 index 00000000..c82c825c --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Home/Sources/RecordEdit/ImageView/HomeEntireImageView.swift @@ -0,0 +1,120 @@ +// +// NamoEntireImageView.swift +// SharedDesignSystem +// +// Created by 박민서 on 12/11/24. +// + +import SwiftUI +import ComposableArchitecture +import SharedDesignSystem +import SharedUtil + +public struct HomeEntireImageView: View { + + @Perception.Bindable var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + ZStack() { + ImageCarouselView(store: store) + + VStack { + TopBar(totalImgCount: store.imgDataList.count, currentImgIndex: $store.currentPage) + Spacer() + } + } + .background(.black) + .namoToastView( + isPresented: $store.showToast, + title: store.toast.content, + isTabBarScreen: false + ) + } + } +} + +// MARK: Top Bar +private extension HomeEntireImageView { + func TopBar(totalImgCount: Int, currentImgIndex: Binding) -> some View { + HStack { + // Back button + Button(action: { + store.send(.tapBackButton) + }, label: { + Image(asset: SharedDesignSystemAsset.Assets.icArrowLeft) + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundStyle(.white) + .frame(width: 32, height: 32) + }) + + Spacer() + + // Image number + HStack(spacing: 10) { + Text("\(currentImgIndex.wrappedValue + 1)") // 표시는 index + 1 + .font(.pretendard(.bold, size: 18)) + .foregroundStyle(.white) + + Text("/") + .font(.pretendard(.bold, size: 18)) + .foregroundStyle(Color.textPlaceholder) + + Text("\(totalImgCount)") + .font(.pretendard(.bold, size: 18)) + .foregroundStyle(Color.textPlaceholder) + } + + Spacer() + + // Download button + Button(action: { + store.send(.tapDownloadButton) + }, label: { + Image(asset: SharedDesignSystemAsset.Assets.icDownload) + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundStyle(.white) + .frame(width: 24, height: 24) + }) + } + .padding(.horizontal, 24) + .padding(.vertical, 14) + .frame(maxWidth: .infinity) + .background(Color.colorBlack.opacity(0.5)) + } +} + +// MARK: ImageCarouselView +private extension HomeEntireImageView { + func ImageCarouselView(store: StoreOf) -> some View { + TabView(selection: Binding( + get: { store.currentPage }, + set: { store.send(.pageChanged(page: $0)) } + )) { + ForEach(store.imgDataList, id: \.id) { imageData in + if let uiImage = UIImage(data: imageData.data) { + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + .tag(store.imgDataList.firstIndex(of: imageData) ?? 0) + } + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .ignoresSafeArea(.container, edges: .bottom) + } +} + +#Preview { + HomeEntireImageView(store: .init(initialState: HomeEntireImageStore.State(imgDataList: [])) { + HomeEntireImageStore() + }) +} diff --git a/Namo_SwiftUI/Projects/Shared/Util/Sources/Model/IdentifiableData.swift b/Namo_SwiftUI/Projects/Shared/Util/Sources/Model/IdentifiableData.swift new file mode 100644 index 00000000..d45cd03e --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/Util/Sources/Model/IdentifiableData.swift @@ -0,0 +1,18 @@ +// +// IdentifiableData.swift +// SharedUtil +// +// Created by 박민서 on 12/11/24. +// + +import Foundation + +public struct IdentifiableData: Identifiable, Equatable { + public let id: UUID + public let data: Data + + public init(id: UUID = UUID(), data: Data) { + self.id = id + self.data = data + } +} diff --git a/Namo_SwiftUI/Projects/Shared/Util/Sources/Utils/Photos.swift b/Namo_SwiftUI/Projects/Shared/Util/Sources/Utils/Photos.swift new file mode 100644 index 00000000..ed57b025 --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/Util/Sources/Utils/Photos.swift @@ -0,0 +1,31 @@ +// +// Photos.swift +// SharedUtil +// +// Created by 박민서 on 12/11/24. +// + +import Photos +import UIKit + +public enum PhotoLibraryError: Error { + case unauthorized + case unknown(Error?) +} + +public func saveImageToPhotoLibrary(_ image: UIImage) async throws { + let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly) + guard status == .authorized else { throw PhotoLibraryError.unauthorized } + + try await withCheckedThrowingContinuation { continuation in + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAsset(from: image) + }) { success, error in + if success { + continuation.resume() + } else { + continuation.resume(throwing: PhotoLibraryError.unknown(error)) + } + } + } +}