diff --git a/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Contents.json b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Contents.json new file mode 100644 index 00000000..b2a331d7 --- /dev/null +++ b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Glyph_ undefined.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Glyph_ undefined 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Glyph_ undefined 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined 1.png b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined 1.png new file mode 100644 index 00000000..75d5ea89 Binary files /dev/null and b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined 1.png differ diff --git a/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined 2.png b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined 2.png new file mode 100644 index 00000000..fe4d797e Binary files /dev/null and b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined 2.png differ diff --git a/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined.png b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined.png new file mode 100644 index 00000000..5f36c0db Binary files /dev/null and b/iOS/traveline/Resources/Images.xcassets/Common/empty.imageset/Glyph_ undefined.png differ diff --git a/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Contents.json b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Contents.json new file mode 100644 index 00000000..b2a331d7 --- /dev/null +++ b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Glyph_ undefined.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Glyph_ undefined 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Glyph_ undefined 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined 1.png b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined 1.png new file mode 100644 index 00000000..c63e2fcd Binary files /dev/null and b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined 1.png differ diff --git a/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined 2.png b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined 2.png new file mode 100644 index 00000000..489a23a5 Binary files /dev/null and b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined 2.png differ diff --git a/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined.png b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined.png new file mode 100644 index 00000000..e8eabc1c Binary files /dev/null and b/iOS/traveline/Resources/Images.xcassets/Common/errorCircle.imageset/Glyph_ undefined.png differ diff --git a/iOS/traveline/Sources/DesignSystem/Common/TLImage.swift b/iOS/traveline/Sources/DesignSystem/Common/TLImage.swift index 5b52c108..3272b13a 100644 --- a/iOS/traveline/Sources/DesignSystem/Common/TLImage.swift +++ b/iOS/traveline/Sources/DesignSystem/Common/TLImage.swift @@ -33,6 +33,8 @@ enum TLImage { static let close = TravelineAsset.Images.closeMedium.image static let logo = TravelineAsset.Images.travelineLogo.image static let`default` = TravelineAsset.Images.default.image + static let empty = TravelineAsset.Images.empty.image + static let errorCircle = TravelineAsset.Images.errorCircle.image } enum Travel { diff --git a/iOS/traveline/Sources/DesignSystem/View/TLEmptyView.swift b/iOS/traveline/Sources/DesignSystem/View/TLEmptyView.swift new file mode 100644 index 00000000..65dac97b --- /dev/null +++ b/iOS/traveline/Sources/DesignSystem/View/TLEmptyView.swift @@ -0,0 +1,142 @@ +// +// TLEmptyView.swift +// traveline +// +// Created by KiWoong Hong on 2024/01/12. +// Copyright © 2024 traveline. All rights reserved. +// + +import UIKit + +class TLEmptyView: UIView { + + enum EmptyViewType { + case search + case timeline + + var image: UIImage { + switch self { + case .search: + return TLImage.Common.errorCircle + case .timeline: + return TLImage.Common.empty + } + } + + var firstText: String { + switch self { + case .search: + return "검색 결과가 없어요!" + case .timeline: + return "아직 작성된 글이 없어요!" + } + } + + var secondText: String { + switch self { + case .search: + return "다른 키워드로 검색해보세요 :)" + case .timeline: + return "나만의 여행 경험을 공유해보세요 :)" + } + } + + var bottomConstants: CGFloat { + switch self { + case .search: + return UIScreen.main.bounds.width + case .timeline: + return UIScreen.main.bounds.width / 3 * 2 + } + } + } + + private enum Metric { + static let imageToLabelSpacing: CGFloat = 16 + static let labelToLabelSpacing: CGFloat = 12 + } + + // MARK: - UI Components + + private let stackView: UIStackView = { + let view = UIStackView() + view.translatesAutoresizingMaskIntoConstraints = false + view.axis = .vertical + view.alignment = .center + view.distribution = .fill + + return view + }() + + private let imageView: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private let firstLabel: TLLabel = { + let label = TLLabel(font: .subtitle2, color: TLColor.white) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + private let secondLabel: TLLabel = { + let label = TLLabel(font: .body2, color: TLColor.white) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + // MARK: - properties + + private let emptyViewType: EmptyViewType + + // MARK: - initialize + + init(type: EmptyViewType) { + self.emptyViewType = type + super.init(frame: .zero) + + setupAttributes() + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +// MARK: - Setup Functions + +private extension TLEmptyView { + + func setupAttributes() { + backgroundColor = TLColor.black + imageView.image = emptyViewType.image + firstLabel.setText(to: emptyViewType.firstText) + secondLabel.setText(to: emptyViewType.secondText) + } + + func setupLayout() { + addSubview(stackView) + stackView.addArrangedSubviews( + imageView, + firstLabel, + secondLabel + ) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: imageView.topAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -emptyViewType.bottomConstants) + ]) + + stackView.setCustomSpacing(Metric.imageToLabelSpacing, after: imageView) + stackView.setCustomSpacing(Metric.labelToLabelSpacing, after: firstLabel) + } + +} diff --git a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/VC/HomeVC.swift b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/VC/HomeVC.swift index 6e9dcf9e..2cb3895f 100644 --- a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/VC/HomeVC.swift +++ b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/VC/HomeVC.swift @@ -113,6 +113,7 @@ private extension HomeVC { self.navigationItem.backBarButtonItem = backBarButtonItem homeSearchView.isHidden = true + homeListView.hideEmptyView() } func setupLayout() { @@ -156,7 +157,6 @@ private extension HomeVC { viewModel.state .map(\.travelList) - .dropFirst() .removeDuplicates() .withUnretained(self) .sink { owner, list in @@ -249,6 +249,18 @@ private extension HomeVC { owner.navigationController?.pushViewController(travelVC, animated: true) } .store(in: &cancellables) + + viewModel.state + .map(\.isEmptyResult) + .withUnretained(self) + .sink { owner, isEmpty in + if isEmpty { + owner.homeListView.showEmptyView() + } else { + owner.homeListView.hideEmptyView() + } + } + .store(in: &cancellables) } func bindListView() { diff --git a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/View/HomeListView.swift b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/View/HomeListView.swift index 0cd7e351..849462fb 100644 --- a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/View/HomeListView.swift +++ b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/View/HomeListView.swift @@ -66,6 +66,8 @@ final class HomeListView: UIView { return refresh }() + private let emptyView: TLEmptyView = .init(type: .search) + // MARK: - Properties private typealias DataSource = UICollectionViewDiffableDataSource @@ -94,6 +96,7 @@ final class HomeListView: UIView { setupLayout() setupDataSource() + setupAttributes() setupSnapshot() } @@ -224,6 +227,14 @@ final class HomeListView: UIView { } } + func hideEmptyView() { + homeCollectionView.backgroundView?.isHidden = true + } + + func showEmptyView() { + homeCollectionView.backgroundView?.isHidden = false + } + @objc private func refreshList() { didRefreshHomeList.send(Void()) } @@ -232,6 +243,11 @@ final class HomeListView: UIView { // MARK: - Setup Functions extension HomeListView { + private func setupAttributes() { + homeCollectionView.backgroundView = emptyView + homeCollectionView.backgroundView?.isHidden = true + } + private func setupLayout() { addSubviews(homeCollectionView) diff --git a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeState.swift b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeState.swift index f890b402..7f46da42 100644 --- a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeState.swift +++ b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeState.swift @@ -27,6 +27,7 @@ struct HomeState: BaseState { var resultFilters: FilterDictionary = .make() var curFilter: Filter? = .emtpy var moveToTravelWriting: Bool = false + var isEmptyResult: Bool = false var isSearching: Bool { homeViewType == .recent || homeViewType == .related diff --git a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeViewModel.swift b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeViewModel.swift index 68515435..853f57d7 100644 --- a/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeViewModel.swift +++ b/iOS/traveline/Sources/Feature/HomeFeature/HomeScene/ViewModel/HomeViewModel.swift @@ -90,6 +90,7 @@ final class HomeViewModel: BaseViewModel newState.travelList = searchResult.travelList newState.homeViewType = .result newState.resultFilters = .make() + newState.isEmptyResult = searchResult.travelList.isEmpty newState.searchQuery = .init( keyword: searchResult.keyword, offset: 2 @@ -99,10 +100,12 @@ final class HomeViewModel: BaseViewModel newState.travelList = travelList newState.searchQuery.offset = 2 newState.searchQuery.keyword = nil + newState.isEmptyResult = travelList.isEmpty case let .showNewList(travelList): newState.travelList = travelList newState.searchQuery.offset = 2 + newState.isEmptyResult = travelList.isEmpty case let .showFilter(type): newState.curFilter = (state.homeViewType == .home) ? state.homeFilters[type] : state.resultFilters[type] diff --git a/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/VC/TimelineDetailVC.swift b/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/VC/TimelineDetailVC.swift index 15edcb03..990ddeb1 100644 --- a/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/VC/TimelineDetailVC.swift +++ b/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/VC/TimelineDetailVC.swift @@ -235,7 +235,6 @@ private extension TimelineDetailVC { viewModel.state .map(\.isEdit) .filter { $0 } - .removeDuplicates() .withUnretained(self) .sink { owner, _ in let timelineDetailInfo = owner.viewModel.currentState.timelineDetailInfo @@ -246,6 +245,7 @@ private extension TimelineDetailVC { timelineDetailInfo: timelineDetailInfo ) owner.navigationController?.pushViewController(timelineEditVC, animated: true) + owner.viewModel.sendAction(.movedToEdit) } .store(in: &cancellables) diff --git a/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/ViewModel/TimelineDetailViewModel.swift b/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/ViewModel/TimelineDetailViewModel.swift index 9f1902f9..13ce77d7 100644 --- a/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/ViewModel/TimelineDetailViewModel.swift +++ b/iOS/traveline/Sources/Feature/TimelineDetailFeature/TimelineDetailScene/ViewModel/TimelineDetailViewModel.swift @@ -14,6 +14,7 @@ enum TimelineDetailAction: BaseAction { case editTimeline case deleteTimeline case translateTimeline + case movedToEdit } enum TimelineDetailSideEffect: BaseSideEffect { @@ -34,6 +35,7 @@ enum TimelineDetailSideEffect: BaseSideEffect { case popToTimeline(Bool) case showTimelineDetailEditing case loadTimelineTranslatedInfo(TimelineTranslatedInfo) + case resetIsEditStatus } struct TimelineDetailState: BaseState { @@ -68,6 +70,9 @@ final class TimelineDetailViewModel: BaseViewModel { @@ -141,6 +142,7 @@ final class TimelineViewModel: BaseViewModel