Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 개인 기록 이미지뷰 구현 #217

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -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<IdentifiableData>
var currentPage: Int
var curretImgData: IdentifiableData? { imgDataList[currentPage] }

// 토스트
var showToast: Bool = false
var toast: Toast = .none
}

public enum Action: BindableAction {
case binding(BindingAction<State>)
case tapBackButton
case tapDownloadButton
case downloadImage(imgData: Data)
case pageChanged(page: Int)
case showToast(Toast)
}

public var body: some ReducerOf<Self> {
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 ""
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HomeEntireImageStore>

public init(store: StoreOf<HomeEntireImageStore>) {
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<Int>) -> 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<HomeEntireImageStore>) -> 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()
})
}
Original file line number Diff line number Diff line change
@@ -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
}
}
31 changes: 31 additions & 0 deletions Namo_SwiftUI/Projects/Shared/Util/Sources/Utils/Photos.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}