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

커스텀 앨범에서 이미지 편집 뷰로 이동 #44

Merged
merged 11 commits into from
Nov 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,44 @@ import Photos

final class CustomAlbumViewController: UIViewController {
// MARK: - Properties
private var imageAsset: PHFetchResult<PHAsset>?
private let imageManager = PHCachingImageManager()
private let imagePicker = UIImagePickerController()

private lazy var albumCollectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
let cellSize = (self.view.bounds.inset(by: self.view.safeAreaInsets).width - 10) / 3
let cellSize = (self.view.bounds.inset(by: self.view.safeAreaInsets).width - 6) / 3
flowLayout.itemSize = CGSize(width: cellSize, height: cellSize)
flowLayout.minimumLineSpacing = 10
flowLayout.minimumInteritemSpacing = 5
flowLayout.minimumLineSpacing = 3
flowLayout.minimumInteritemSpacing = 2
flowLayout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)

return collectionView
}()
private let viewModel: CustomAlbumViewModel

// MARK: - Initializer
init(viewModel: CustomAlbumViewModel) {
self.viewModel = viewModel

super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
self.viewModel = CustomAlbumViewModel()

super.init(nibName: nil, bundle: nil)
}

// MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()

setup()
configureConstraints()
viewModel.action(.viewDidLoad)
}

// MARK: - Setup & Configure
private func setup() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

imageAsset = PHAsset.fetchAssets(with: fetchOptions)
imagePicker.delegate = self
albumCollectionView.delegate = self
albumCollectionView.dataSource = self
Expand All @@ -49,9 +57,9 @@ final class CustomAlbumViewController: UIViewController {

// MARK: - Open Camera
private func openCamera() {
if (UIImagePickerController.isSourceTypeAvailable(.camera)) {
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePicker.sourceType = .camera
navigationController?.pushViewController(imagePicker, animated: true)
navigationController?.show(imagePicker, sender: nil)
} else {
// TODO: - 카메라 접근 권한 Alert
}
Expand All @@ -67,14 +75,14 @@ extension CustomAlbumViewController: UICollectionViewDelegate {
if indexPath.item == 0 {
self.openCamera()
} else {
guard let asset = self.imageAsset?[indexPath.item - 1] else { return }
imageManager.requestImage(
for: asset,
targetSize: .zero,
contentMode: .default,
options: nil
) { image, _ in
// TODO: - 이미지 편집 뷰 로 이동
guard let asset = viewModel.photoAsset?[indexPath.item - 1] else { return }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. 뷰모델의 속성에 직접 접근하는 것 대신 함수를 만들어주면 어떨까요?
func photo(at indexPath: indexPath) -> 사진 느낌?

Copy link
Collaborator Author

@k2645 k2645 Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분도 고민을 많이 한 지점이었는데 cell의 delegate나 dataSource 관련된 함수 내부에서 ViewModel과 어떻게 바인딩을 해야할지를 모르겠어서 저렇게 직접 접근하는 방식을 사용했던 것 같습니다..

데이터의 단방향 flow를 만들고 싶어서 ViewModelInput이라는 열거형을 만들고 Input와 output을 나눠 관리하려했는데 메서드를 넣으면 훔... 그래도 확실히 뷰모델의 속성에 직접 접근하는게 어색해보일 수 있을 것 같네요.. 일단 조금 더 고민해보겠습니다 .ᐟ.ᐟ

Task {
await LocalPhotoManager.shared.requestImage(with: asset) { [weak self] image in
guard let self else { return }
let editViewController = EditPhotoViewController()
editViewController.setPhoto(image: image)
self.navigationController?.pushViewController(editViewController, animated: true)
}
}
}
}
Expand All @@ -86,8 +94,8 @@ extension CustomAlbumViewController: UICollectionViewDataSource {
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
guard let imageAsset else { return 1 }
return imageAsset.count + 1
guard let assetNum = viewModel.photoAsset?.count else { return 1 }
return assetNum + 1
}

func collectionView(
Expand All @@ -102,18 +110,14 @@ extension CustomAlbumViewController: UICollectionViewDataSource {
if indexPath.item == 0 {
cell.setPhoto(.photo)
} else {
guard let asset = imageAsset?[indexPath.item - 1] else { return cell }
guard let asset = viewModel.photoAsset?[indexPath.item - 1] else { return cell }
let cellSize = cell.bounds.size
imageManager.requestImage(
for: asset,
targetSize: cellSize,
contentMode: .aspectFill,
options: nil
) { image, _ in
cell.setPhoto(image)
Task {
await LocalPhotoManager.shared.requestImage(with: asset, cellSize: cellSize) { image in
cell.setPhoto(image)
}
}
}

return cell
}
}
Expand All @@ -122,11 +126,13 @@ extension CustomAlbumViewController: UICollectionViewDataSource {
extension CustomAlbumViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
dismiss(animated: true, completion: nil)
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
// TODO: - 이미지 편집 뷰로 이동
let editViewController = EditPhotoViewController()
editViewController.setPhoto(image: image)
self.navigationController?.pushViewController(editViewController, animated: true)
}
dismiss(animated: true, completion: nil)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import MHFoundation
import Photos
import Combine

final class CustomAlbumViewModel {
enum Input {
case viewDidLoad
}
// MARK: - Properties
@Published private(set) var photoAsset: PHFetchResult<PHAsset>?

func action(_ input: CustomAlbumViewModel.Input) {
switch input {
case .viewDidLoad:
fetchPhotoAssets()
}
}

private func fetchPhotoAssets() {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

photoAsset = PHAsset.fetchAssets(with: fetchOptions)
}
}
Comment on lines +19 to +26
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사진앱에서 사진을 가져오는 코드인가요? 그렇다면 영상과 함께 가져와야하는지 고민되네요! 아니라면 넘어가주세요! ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 24번째 줄 옵션을 넣어서 사진만 가져오게끔 만들었습니다 ! 아무래도 해당 화면은 이미지를 넣을 목적으로 만든 앨범 화면이라 사진만 불러오게 하는 것이 적절할 것 같다고 판단해서 저렇게 만들었습니다 !

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홍 나중에 비디오 자체를 가져오기 전에 썸네일만 가져오도록 하는 방식으로도 최적화를 꾀할 수 있을 것같긴 합니다. (나중이야기)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저거 Asset의 mediaType을 대충 비디오로 해주면 알아서 썸네일만 가져옵니당 .ᐟ.ᐟ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 좋읍니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 안좋게 되었습니다..

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import UIKit
import Photos

actor LocalPhotoManager {
static let shared = LocalPhotoManager()

private let imageManager = PHImageManager()
private let imageRequestOptions: PHImageRequestOptions = {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat

return options
}()

private init() { }

func requestImage(
with asset: PHAsset?,
cellSize: CGSize = .zero,
completion: @escaping @MainActor (UIImage?) -> Void
) {
guard let asset else { return }

imageManager.requestImage(
for: asset,
targetSize: cellSize,
contentMode: .aspectFill,
options: imageRequestOptions,
resultHandler: { image, _ in
Task { await completion(image) }
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

public final class EditPhotoViewController: UIViewController {
// MARK: - Properties
private let photoView = UIImageView()
private let clearView = UIView.dimmedView(opacity: 0)
private let dimmedView1 = UIView.dimmedView(opacity: 0.5)
private let dimmedView2 = UIView.dimmedView(opacity: 0.5)
private let dividedLine1 = UIView.dividedLine()
private let dividedLine2 = UIView.dividedLine()
private let photoView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .clear

return imageView
}()
private let captionTextField: UITextField = {
let textField = UITextField()
textField.attributedPlaceholder = NSAttributedString(
Expand Down Expand Up @@ -75,7 +81,11 @@
navigationController?.navigationBar.titleTextAttributes = [
NSAttributedString.Key.font: UIFont.ownglyphBerry(size: 17),
NSAttributedString.Key.foregroundColor: UIColor.white]
let leftBarButton = UIBarButtonItem(title: "닫기")
let closeAction = UIAction { [weak self] _ in
guard let self else { return }
self.navigationController?.popViewController(animated: true)
}
let leftBarButton = UIBarButtonItem(title: "닫기", primaryAction: closeAction)
Comment on lines +84 to +88
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
비동기 작업 전 optional unwrapping을 하시면 weak의 효과가 사라지는 것으로 알고 있습니다.
https://medium.com/@sunghyun_kim/swift-task에서-weak-self는-언제-쓸까요-f7ccf78e22bb

Copy link
Collaborator Author

@k2645 k2645 Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 해당 부분에 대해서 저도 이전 학습 스프린트때 학습한 적이 있는데 weak의 효과가 사라지는 경우는 Task 코드블럭 내부에서만 해당하는 것으로 알고있습니다 ! 위 클로저는 UIAction으로 Task 블럭이 아니기 때문에 self를 언래핑해도 weak 참조가 가능한 것으로 알고있습니다.

추가로 위 코드는 비동기 작업을 해당 클로저 내부에서 하고있지 않기 때문에 괜찮지 않을까요??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 생각에는 윤철님 말씀이 맞는 것같습니다. 다만, weak self 목적이 강한순환참조를 방지하려는 목적이기 때문에, 저렇게 사용하는 것자체는 동의합니다. Task나 저 Action이나 모두 escaping closure이기 때문에 weak self로 캡쳐한 후 guard let self 를 쓰면 해당 guard문이 실행될 때부터 강하게 붙잡습니다.(weak의 효과가 사라짐)
참고자료

leftBarButton.setTitleTextAttributes(
[NSAttributedString.Key.font: UIFont.ownglyphBerry(size: 17),
NSAttributedString.Key.foregroundColor: UIColor.white],
Expand Down Expand Up @@ -158,20 +168,24 @@

private func configureButtonAction() {
let cropButtonAction = UIAction { _ in
// TODO: - Crop Action

Check warning on line 171 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- Crop Action) (todo)

Check warning on line 171 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- Crop Action) (todo)
}
let rotateButtonAction = UIAction { _ in
// TODO: - Rotate Action

Check warning on line 174 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- Rotate Action) (todo)

Check warning on line 174 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- Rotate Action) (todo)
}
let drawButtonAction = UIAction { _ in
// TODO: - Draw Action

Check warning on line 177 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- Draw Action) (todo)

Check warning on line 177 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- Draw Action) (todo)
}
cropButton.addAction(cropButtonAction, for: .touchUpInside)
rotateButton.addAction(rotateButtonAction, for: .touchUpInside)
drawButton.addAction(drawButtonAction, for: .touchUpInside)
}

func setPhoto(image: UIImage?) {
photoView.image = image
}
}

extension EditPhotoViewController: UITextFieldDelegate {
// TODO: - TextField의 텍스트 처리

Check warning on line 190 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- TextField의 텍스트 처리) (todo)

Check warning on line 190 in MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (- TextField의 텍스트 처리) (todo)
}
Loading