[README] WishList
WishList ✨
- 나만의 위시리스트를 관리할 수 있는 앱
- github
기능
✅ Wish 추가
- 사진, 태그, 이름, 메모, 링크, 장소로 이루어진 위시 추가 기능
- PHPicker를 활용한 사진 추가
- MapKit을 활용한 장소 추가
✅ Wish 수정, 삭제
✅ Wish 조회
- 화살표 버튼 클릭 시 해당 링크 사파리로 연결
✅ Wish 검색
- 태그와 이름을 기반으로 검색
✅ 즐겨찾는 Wish
✅ 공유 기능
- 공유 시 해당 링크 Wish 추가
✅ 사진 편집
- Mantis 라이브러리를 사용한 사진 편집 기능
✅ 현재 위치
- 현재 위치 Wish 추가
✅ 장소 검색
- 장소 검색 시 해당 위치 추가
✅ 지도 마커 표시
- 장소가 추가된 Wish를 지도에 마커로 표시
- 마커 클릭 시 해당 Wish로 이동
설계
✅ ViewController 구성
-
FavoriteWishViewController
-
MapViewController
-
SearchWishViewController
-
AddWishViewController
-
SelectWishViewController
✅ ViewCotroller의 역할
MainViewController
- 나의 WishList를 보여준다.
FavoriteWishViewController
- 관심 WishList를 보여준다.
MapViewController
- 현재 위치를 보여준다.
- 장소를 추가한 Wish의 위치를 마커로 보여준다.
- 현재 위치와 검색한 위치를 Wish로 추가 할 수 있다.
SearchPlaceViewController
- 위치를 검색할 수 있다.
SearchWishViewController
- 검색어가 포함된 Wish를 이름, 태그 별로 검색할 수 있다.
AddWishViewController
- 사진, 이름, 태그, 메모, 링크, 장소의 Wish를 추가할 수 있다.
EditImageViewController
- 기기에서 선택한 사진을 편집할 수 있다.
SelectWishViewController
- 선택한 Wish를 보여준다.
ShowImageViewController
- 선택한 image를 원본 크기로 보여준다.
✅ ViewModel의 역할
WishViewModel
- Wish의 데이터를 불러온다.
- Wish 데이터를 관리한다.
FavoriteWishViewModel
- FavoriteWish(관심 Wish)를 불러온다.
FavoriteWishViewModel
- FilterWish(검색 Wish)를 불러온다.
구현
✅ Notification
Notification
등록된 노티피케이션에 노티피케이션 센터를 통해 정보를 전달하기 위한 구조체.
주요 프로퍼티
- var name: Notification.Name // 알림을 식별하는 태그
- var object: Any? // 발송자가 옵저버에게 보내려고 하는 객체. 주로 발송자 객체를 전달하는데 쓰임
- var userInfo:[AnyHashable: Any]? // 노티피케이션과 관련된 값 또는 객체의 저장소
NotificationCenter
Notification을 발송하면 NotificationCenter에서 메세지를 전달한 옵저버를 처리할 때까지 대기합니다. 즉, 흐름이 동기적(synchronous)으로 흘러갑니다. 노티피케이션을 비동기적으로 사용하려면 NotificationQueue를 사용하면 된다.
NotificationCenter 구현 순서
1. 옵저버 등록
class SelectWishViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
// NotificationCenter에 옵저버 등록
NotificationCenter.default.addObserver(self, selector: #selector(convertToUIImageComplete(_ :)), name: NotificationName.ChangeImageNotification, object: nil)
}
2. 발송
extension WishViewModel {
func changeUIImage(index: Int){
ChangeUIImage.changeUIImage(imageURL: manager.wishs[index].imgURL){ [weak self] image in
self?.manager.wishs[index].img = image
// NoticationCenter에 발송
NotificationCenter.default.post(name: NotificationName.ChangeImageNotification, object: nil)
}
}
3. 호출
class SelectWishViewController: UIViewController {
@objc func convertToUIImageComplete(_ noti: Notification){
DispatchQueue.main.async {
self.indicator.stopAnimating()
self.presentAddWishListVC()
}
}
4. 옵저버 제거
- Notification 중복 방지를 위해 NoticationCenter에서 옵저버를 제거 해줘야된다.
class SelectWishViewController: UIViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
// NotificationCenter에서 옵저버 제거
NotificationCenter.default.removeObserver(self, name: NotificationName.ChangeImageNotification, object: nil)
}
✅ PHPicker
https://zeddios.tistory.com/1052
https://velog.io/@sainkr/TIL-2021.02.02
- UIImagePickerController에 추가로 multiselect / zoom in or out / search 를 지원함.
- iOS 14 이상부터 쓸 수 있다.
- 따로 권한 요청 팝업이 뜨지 않는다.
1. import PhotosUI
2. PHPickerConfiguration 생성
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 5
configuration.filter = .any(of: [.images])
- selectionLimit : 최대 asset 수를 지정하는 프로퍼티, 기본 값은 1 무한 대는 0을 의미한다.
- filter: 이미지, 비디오, 라이브포토 필터 선택. nil이면 모두를 의미한다.
3. PHPickerConfiguration을 갖는 PHPickerViewController 생성
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
picker.modalPresentationStyle = .fullScreen
self.present(picker, animated: true, completion: nil)
4. PHPickerViewControllerDelegate
extension AddImageViewController: PHPickerViewControllerDelegate {
@available(iOS 14, *)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let loadUIImage = LoadUIImage(results.map(\.itemProvider))
loadUIImage.image{ images in
self.presentEditImageViewController(images)
}
}
}
- itemProvider로 Array 값을 가져오고 iterator를 사용하여 데이터를 읽는다.
- 데이터를 load할 수 있다면 UIImage Type으로 load 한다.
class LoadUIImage{
private var itemProviders: [NSItemProvider] = []
init(_ itemProvdiers: [NSItemProvider]){
self.itemProviders = itemProvdiers
}
func image(completion: @escaping (([UIImage]) -> ())){
var iterator: IndexingIterator<[NSItemProvider]> = itemProviders.makeIterator()
var images: [UIImage] = []
while true {
guard let itemProvider = iterator.next(), itemProvider.canLoadObject(ofClass: UIImage.self) else { break }
itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let error = error {
print("loadImageError : \(error)")
return
}
guard let image = image as? UIImage else { return }
images.append(image)
if images.count == self.itemProviders.count{
completion(images)
}
}
}
}
}
✅ Dynamic cell sizing
https://ntomios.tistory.com/15
https://velog.io/@sainkr/TIL-hb5ddnvb
UICollectionViewCell
class AddTagCollectionViewCell: UICollectionViewCell{
static let identifier = "AddTagCollectionViewCell"
private let titleLabel: UILabel = UILabel()
static func fittingSize(availableHeight: CGFloat, tag: String) -> CGSize {
let cell = AddTagCollectionViewCell()
cell.configure(tag: tag)
let targetSize = CGSize(
width: UIView.layoutFittingCompressedSize.width,
height: availableHeight)
return cell.contentView.systemLayoutSizeFitting(
targetSize,
withHorizontalFittingPriority: .fittingSizeLevel,
verticalFittingPriority: .required)
}
func configure(tag: String) {
titleLabel.text = tag
}
// ...
- targetSize의 width는 UIView.layoutFittingCompressedSize.width를 활용하여 뷰를 가능한 작게만들어 주었고 height은 입력받은 값으로 고정해주었다.
- systemLayoutSizeFitting 함수를 통해 Dynamic cell sizing을 구현하였다.
systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)
특정 뷰를 width, height의 우선순위에 따라 targetSize에 맞는 Size를 계산하여 반환해주는 함수
- targetSize
- layoutFittingCompressedSize : 뷰를 가능한 작게
- layoutFittingExpandedSize : 뷰를 가능한 크게 - horizontalFittingPriority (width 우선순위)
- content size에 맞게 늘어나야 함
- 우선순위가 낮은 fittingSizeLevel로 설정 - verticalFittingPriority (height 우선순위)
- targetSize의 height에 반드시 맞출 필요가 있으므로 가장 우선순위가 높은 required로 설정 - 우선순위
required > defaultHigh > defaultLow > fittingSizeLevel
UICollectionViewDelegateFlowLayout
// MARK: - UICollectionViewDelegateFlowLayout
extension AddTagViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return AddTagCollectionViewCell.fittingSize(availableHeight: 45, tag: wishViewModel.tag(index: wishViewModel.lastWishIndex, tagIndex: indexPath.item))
}
}
✅ Mantis
https://velog.io/@sainkr/TIL-2021.03.06
- Image Edit(Crop, Rotation 등) Library
- cropViewController 띄우기
let cropViewController = Mantis.cropViewController(image: 해당 이미지 (UIImage))
cropViewController.delegate = self
cropViewController.modalPresentationStyle = .fullScreen
self.present(cropViewController, animated: true, completion: nil)
- 완료 됐을 때
- cropViewControllerDidCrop() : Done 버튼 눌렀을 때 실행
- cropViewControllerDidCancel() : Cancel 버튼 눌렀을 때 실행
extension EditImageViewController: CropViewControllerDelegate{
func cropViewControllerDidCrop(_ cropViewController: CropViewController, cropped: UIImage, transformation: Transformation) {
images[currentPage] = cropped
updatePageViewController()
dismiss(animated: true, completion: nil)
}
func cropViewControllerDidCancel(_ cropViewController: CropViewController, original: UIImage) {
dismiss(animated: true, completion: nil)
}
}
✅ SnapKit
- constraint을 좀 더 간편하게 사용할 수 있는 오픈 라이브러리
SnapKit을 사용하지 않았을 때
titleBackView.translatesAutoresizingMaskIntoConstraints = false
titleBackView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
titleBackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5).isActive = true
titleBackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
SnapKit을 사용했을 때
titleBackView.snp.makeConstraints{(make) in
make.top.equalToSuperview()
make.leading.equalToSuperview().inset(5)
make.bottom.equalToSuperview()
}
- titleBackView.translatesAutoresizingMaskIntoConstraints = false을 내부에서 해주기 때문에 적어주지 않아도 됨.
translatesAutoresizingMaskIntoConstraints = false 를 쓰는 이유
translatesAutoresizingMaskIntoConstraints = false를 해주지 않고 코드에서 제약을 걸어주면 translatesAutoresizingMaskIntoConstraints = true가 되어 AutorisizingMask를 통해 제약이 자동 생성 되면서 코드로 걸어준 제약과 충돌하기 때문에 translatesAutoresizingMaskIntoConstraints를 false로 설정해줘야 된다.
✅ Firebase
https://velog.io/@sainkr/TIL-2021.04.01
1. Firebase initialize
import Firebase
import FirebaseStorage
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Use Firebase library to configure APIs
FirebaseApp.configure()
2. FIRDatabaseReference, Storage 가져오기
let db = Database.database().reference().child("myWhisList")
let storage = Storage.storage()
3. 데이터 쓰기
- setValue()
- 지정된 참조에 데이터를 저장하고 해당 경로의 기존 데이터를 모두 바꾼다.
- NSString, NSNumber, NSDictionary, NSArray와 같은 유형을 전달함.
db
.child(String(wish.timestamp))
.setValue([
"timestamp" : wish.timestamp,
"name": wish.name ,
"tag" : wish.tag,
"imgURL" : ["-"],
"memo" : wish.memo,
"link" : wish.link,
"placeName" : wish.place?.name ?? "-",
"placeLat": wish.place?.lat ?? 0,
"placeLng": wish.place?.lng ?? 0,
"favorite": wish.favorite])
4. 데이터 읽기
- observeSingleEvent()
- 관찰자를 사용하여 데이터 한 번 읽기
- 한 번 로드된 후 자주 변경되지 않거나 능동적으로 수신 대기할 필요가 없는 데이터에 유용. 예를 들어 사용자의 프로필 같은 경우
self.db.observeSingleEvent(of:.value) { (snapshot) in
guard let wishValue = snapshot.value as? [String:Any] else { return }
// print("---> snapshot : \(wishValue.values)")
do {
// 1. DB에서 받아온 Foundation 값을 json 데이터로 만들어 준다.
let data = try JSONSerialization.data(withJSONObject: Array(wishValue.values), options: [])
let decoder = JSONDecoder()
// 2. [WishDB]로 디코딩 해준다.
let wish = try decoder.decode([WishDB].self, from: data)
}catch {
print("---> error : \(error)")
}
}
UIImage를 Storage에 저장 후 ImageURL 값 가져오기.
1. UIImage를 Jpeg 형식의 데이터로 변경
// compressionQuailty : 0.0 ~ 1.0 의 값을 가지며 JPEG 이미지의 품질을 나타냄. 0.0은 최대 압축 (lowest quailty) 1.0은 최소 압축 (best quailty)
let data = image.jpegData(compressionQuality: 0.1)!
2. Storage에 파일 업로드
- putData()
- Storage에 파일을 업로드하는 가장 간단한 방법
- name, size, contentType 등 메타데이터를 포함할 수 있다.
- NSData 객체를 취하고 FIRStorageUploadTask를 반환하며, 이를 사용하여 업로드를 관리하고 업로드 상태를 모니터링할 수 있다.
let metaData = StorageMetadata()
metaData.contentType = "image/png" // contentType metaData 설정
let imageName = "\(wish.timestamp)\(i)"
storage.reference().child(imageName).putData(data, metadata: metaData) { (data, err) in
if let error = err {
print("--> error:\(error.localizedDescription)")
return
}
}
- downloadURL() : Storage에 올린 데이터의 URL을 가져옴.
self.storage.reference().child(imageName).downloadURL { (url, err) in
if let error = err {
print("--> error:\(error.localizedDescription)")
return
}
guard let url = url else {
print("--> urlError")
return
}
imageURL.append(url.absoluteString)
if imageURL.count == wish.img.count {
completion(imageURL)
}
}
전체 코드
private func convertUIImagetoImageURL(wish: Wish, completion: @escaping (_ imageURL: [String]) -> Void){
var imageURL: [String] = []
for i in wish.img.indices{
let image: UIImage = wish.img[i]
// 지정된 이미지를 JPEG 형식으로 포함하는 데이터 개체를 반환합니다., JPEG 이미지의 품질 최대 압축 ~ 최소 압축
let data = image.jpegData(compressionQuality: 0.1)!
let metaData = StorageMetadata()
metaData.contentType = "image/png"
let imageName = "\(wish.timestamp)\(i)"
storage.reference().child(imageName).putData(data, metadata: metaData) { (data, err) in
if let error = err {
print("--> error:\(error.localizedDescription)")
return
}
self.storage.reference().child(imageName).downloadURL { (url, err) in
if let error = err {
print("--> error: \(error.localizedDescription)")
return
}
guard let url = url else {
print("--> urlError")
return
}
imageURL.append(url.absoluteString)
if imageURL.count == wish.img.count {
completion(imageURL)
}
}
}
}
}
✅ Kingfisher
캐시(cache)
주기억 장치에 읽어들인 명령이나 프로그램들로 채워지는 버퍼 형태의 고속 기억 장치. 중앙 처리 장치가 명령이 필요하게 되면, 맨 먼저 액세스하는 것은 주기억 장치가 아니라 캐시 메모리이다. 자주 액세스하는 데이터나 프로그램 명령을 반복해서 검색하지 않고도 즉각 사용할 수 있도록 저장해두는 영역이다.
[네이버 지식백과] 캐시 [cache] (컴퓨터인터넷IT용어대사전, 2011. 1. 20., 전산용어사전편찬위원회)
- 이미지 캐싱을 쉽게 할 수 있게 해주는 라이브러리.
- UIImageView, NSImageView, UIButton, NsButton에서 사용 가능.
let url = URL(string:https://firebasestorage.googleapis.com/9a567ad81b57)
imageView.kf.setImage(with: url)
Kingfisher 실행 순서
https://1consumption.github.io/posts/about-kingfisher(1)/ 를 참고하였습니다.
- url.absoluteString을 key로 캐싱 된 이미지가 있는지 확인합니다.
- 만약 cache(memory나 disk)에서 이미지를 찾으면 imageView.image에 설정해줍니다.
- 그렇지 않다면, request를 생성하고 url로 부터 다운로드합니다.
- 다운로드된 데이터를 UIImage 객체로 변환합니다.
- memory cache에 이미지를 cache하고 disk cache에 데이터를 저장합니다.
- 이미지를 표시하기 위해 imageView.image를 설정하세요.
Author And Source
이 문제에 관하여([README] WishList), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sainkr/README-WishList저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)