Skip to content

Commit 7650e4e

Browse files
committed
feat: 코스 발견 성능 최적화 및 UX 개선
- 탭 재탭 시 스크롤 투 탑만 수행 (불필요한 API 호출 제거) - reloadData() → reloadSections() 전환 (Section 4만 갱신) - 배너 캐러셀 셀 재사용 최적화 (viewWithTag 패턴) - 배너 가로 스와이프 시 세로 스크롤 충돌 방지 - 배너 영속성 (prepareForReuse 상태 유지, skipReload) - pull-to-refresh 추가 (Section 4 전용) - iOS 26 Liquid Glass 탭바 대응 - 탭 아이콘 rendering intent template 전환 - pull-to-refresh 시 LoadingIndicator 이중 표시 제거 - courseList 사전 삭제 제거 (data source 불일치 방지)
1 parent 034d561 commit 7650e4e

File tree

12 files changed

+147
-71
lines changed

12 files changed

+147
-71
lines changed

Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UITabBar+.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import UIKit
99

1010
extension UITabBar {
11-
11+
1212
static func clearShadow() {
1313
UITabBar.appearance().shadowImage = UIImage()
1414
UITabBar.appearance().backgroundImage = UIImage()

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_course_discove_fill.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_course_discover.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_course_draw.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_course_draw_fill.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_mypage.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_mypage_fill.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_storage.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_storage_fill.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"version" : 1
2222
},
2323
"properties" : {
24-
"template-rendering-intent" : "original"
24+
"template-rendering-intent" : "template"
2525
}
2626
}

Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/AdImageCollectionViewCell.swift

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -166,23 +166,8 @@ final class AdImageCollectionViewCell: UICollectionViewCell {
166166
override func prepareForReuse() {
167167
super.prepareForReuse()
168168
stopAutoScroll()
169-
cancelShimmerTimeout()
170-
171-
// 상태 초기화
172-
adsResponseCount = 0
173-
carouselShown = false
174-
isAd1Loaded = false
175-
isAd2Loaded = false
176-
currentPage = 0
177-
pages = imgBanners.map { .image($0) }
178-
179-
// UI 초기 상태로 리셋
180-
bannerCollectionView.isHidden = true
181-
gradientView.isHidden = true
182-
pageControlStack.isHidden = true
183-
adLabelContainer.alpha = 0
184-
shimmerView.isHidden = false
185-
shimmerView.alpha = 1
169+
// 이미 캐러셀이 표시된 상태면 리셋하지 않음
170+
// 새 광고 로드가 필요할 때만 리셋 (setRootViewController에서 처리)
186171
}
187172

188173
deinit {
@@ -195,13 +180,20 @@ final class AdImageCollectionViewCell: UICollectionViewCell {
195180

196181
extension AdImageCollectionViewCell {
197182

198-
func setRootViewController(_ viewController: UIViewController) {
183+
/// 이미 로드된 광고가 있으면 shimmer 없이 바로 표시
184+
func setRootViewController(_ viewController: UIViewController, skipReload: Bool = false) {
199185
guard shouldShowAds else {
186+
if skipReload && carouselShown { return }
200187
skipAdsAndShowCarousel()
201188
return
202189
}
203190
bannerView1.rootViewController = viewController
204191
bannerView2.rootViewController = viewController
192+
193+
if skipReload && carouselShown {
194+
// 이미 로드된 상태 — shimmer 없이 바로 표시
195+
return
196+
}
205197
loadAdMobBanners()
206198
}
207199

@@ -456,6 +448,11 @@ extension AdImageCollectionViewCell {
456448
return Self.imagePageInterval
457449
}
458450

451+
func resumeAutoScrollIfNeeded() {
452+
guard carouselShown, autoScrollWork == nil else { return }
453+
scheduleNextScroll()
454+
}
455+
459456
private func startAutoScroll() {
460457
stopAutoScroll()
461458

@@ -533,22 +530,36 @@ extension AdImageCollectionViewCell {
533530

534531
extension AdImageCollectionViewCell: UIScrollViewDelegate {
535532

533+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
534+
guard scrollView === bannerCollectionView else { return }
535+
// 배너 가로 스와이프 시 상위 세로 스크롤 잠금
536+
findParentScrollView()?.isScrollEnabled = false
537+
}
538+
539+
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
540+
guard scrollView === bannerCollectionView else { return }
541+
if !decelerate {
542+
findParentScrollView()?.isScrollEnabled = true
543+
}
544+
}
545+
536546
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
537547
guard scrollView === bannerCollectionView, scrollView.frame.width > 0 else { return }
548+
549+
findParentScrollView()?.isScrollEnabled = true
550+
538551
currentPage = Int(scrollView.contentOffset.x / scrollView.frame.width)
539552

540553
// 무한 스크롤: 양 끝에 도달하면 중간 범위로 리셋
541554
if !pages.isEmpty {
542555
if currentPage < pages.count {
543-
// 왼쪽 끝 → 중간 범위로 점프
544556
currentPage += pages.count
545557
bannerCollectionView.scrollToItem(
546558
at: IndexPath(item: currentPage, section: 0),
547559
at: .centeredHorizontally,
548560
animated: false
549561
)
550562
} else if currentPage >= pages.count * 2 {
551-
// 오른쪽 끝 → 중간 범위로 점프
552563
currentPage -= pages.count
553564
bannerCollectionView.scrollToItem(
554565
at: IndexPath(item: currentPage, section: 0),
@@ -562,6 +573,17 @@ extension AdImageCollectionViewCell: UIScrollViewDelegate {
562573
updateAdPillVisibility()
563574
scheduleNextScroll()
564575
}
576+
577+
private func findParentScrollView() -> UIScrollView? {
578+
var view = superview
579+
while let v = view {
580+
if let scrollView = v as? UIScrollView, scrollView !== bannerCollectionView {
581+
return scrollView
582+
}
583+
view = v.superview
584+
}
585+
return nil
586+
}
565587
}
566588

567589
// MARK: - Carousel Setup
@@ -651,31 +673,34 @@ extension AdImageCollectionViewCell: UICollectionViewDelegate, UICollectionViewD
651673

652674
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
653675
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BannerCell", for: indexPath)
654-
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
655-
656676
guard !pages.isEmpty else { return cell }
657677

658678
let pageIndex = indexPath.item % pages.count
659679
switch pages[pageIndex] {
660680
case .image(let image):
661681
cell.contentView.backgroundColor = .clear
662-
let imageView = UIImageView().then {
663-
$0.image = image
664-
$0.contentMode = .scaleAspectFill
665-
$0.clipsToBounds = true
666-
}
667-
cell.contentView.addSubview(imageView)
668-
imageView.snp.makeConstraints {
669-
$0.edges.equalToSuperview()
682+
// 기존 UIImageView 재사용 — 매번 생성/제거 방지
683+
if let imageView = cell.contentView.viewWithTag(100) as? UIImageView {
684+
imageView.image = image
685+
} else {
686+
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
687+
let imageView = UIImageView()
688+
imageView.tag = 100
689+
imageView.contentMode = .scaleAspectFill
690+
imageView.clipsToBounds = true
691+
imageView.image = image
692+
cell.contentView.addSubview(imageView)
693+
imageView.snp.makeConstraints { $0.edges.equalToSuperview() }
670694
}
671695
case .ad(let adBannerView):
672696
cell.contentView.backgroundColor = .w1
673-
adBannerView.removeFromSuperview()
674-
cell.contentView.addSubview(adBannerView)
675-
adBannerView.snp.makeConstraints {
676-
$0.centerX.equalToSuperview()
677-
$0.centerY.equalToSuperview()
678-
$0.leading.trailing.equalToSuperview()
697+
if adBannerView.superview !== cell.contentView {
698+
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
699+
cell.contentView.addSubview(adBannerView)
700+
adBannerView.snp.makeConstraints {
701+
$0.centerY.equalToSuperview()
702+
$0.leading.trailing.equalToSuperview()
703+
}
679704
}
680705
}
681706
return cell

0 commit comments

Comments
 (0)