Skip to content

Commit bfe05da

Browse files
committed
[Feat] #192 - 줌 기능 구현
1 parent 8cd4755 commit bfe05da

2 files changed

Lines changed: 124 additions & 12 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//
2+
// ZoomableImageView.swift
3+
// Neki-iOS
4+
//
5+
// Created by OneTen on 3/28/26.
6+
//
7+
8+
import SwiftUI
9+
import Kingfisher
10+
11+
struct ZoomableImageView: View {
12+
let imageURL: URL?
13+
let isCurrent: Bool
14+
15+
@State private var scale: CGFloat = 1.0
16+
@State private var lastScale: CGFloat = 1.0
17+
@State private var offset: CGSize = .zero
18+
@State private var lastOffset: CGSize = .zero
19+
20+
var body: some View {
21+
GeometryReader { geo in
22+
KFImage(imageURL)
23+
.resizable()
24+
.placeholder {
25+
ProgressView()
26+
.frame(maxWidth: .infinity, maxHeight: .infinity)
27+
}
28+
.retry(maxCount: 3, interval: .seconds(5))
29+
.cancelOnDisappear(true)
30+
.aspectRatio(contentMode: .fit)
31+
.frame(maxWidth: .infinity, maxHeight: .infinity)
32+
.scaleEffect(scale)
33+
.offset(offset)
34+
.gesture(
35+
MagnifyGesture()
36+
.onChanged { value in
37+
let delta = value.magnification / lastScale
38+
lastScale = value.magnification
39+
scale = min(max(scale * delta, 0.8), 5.0) // 0.8 ~ 5배까지
40+
}
41+
.onEnded { _ in
42+
lastScale = 1.0
43+
if scale <= 1.0 {
44+
clampOffset(geo: geo)
45+
}
46+
}
47+
)
48+
49+
// 확대된 상태에서 이미지 이동
50+
.highPriorityGesture(
51+
DragGesture()
52+
.onChanged { value in
53+
// 드래그 가능한 최대 범위 계산
54+
let maxX = max(0, (geo.size.width * (scale - 1)) / 2)
55+
let maxY = max(0, (geo.size.height * (scale - 1)) / 2)
56+
57+
let proposedWidth = lastOffset.width + value.translation.width
58+
let proposedHeight = lastOffset.height + value.translation.height
59+
60+
// 계산된 범위 내에서만 offset 적용
61+
offset = CGSize(
62+
width: min(max(proposedWidth, -maxX), maxX),
63+
height: min(max(proposedHeight, -maxY), maxY)
64+
)
65+
}
66+
.onEnded { _ in
67+
lastOffset = offset
68+
},
69+
including: scale > 1.0 ? .gesture : .subviews
70+
)
71+
72+
// 더블 탭 줌
73+
.onTapGesture(count: 2) {
74+
if scale > 1.0 {
75+
resetZoom()
76+
} else {
77+
withAnimation(.spring()) {
78+
scale = 2.5
79+
}
80+
}
81+
}
82+
83+
// 스와이프로 다음 사진으로 넘어갔을 때 이전 사진 줌 초기화
84+
.onChange(of: isCurrent) { _, newValue in
85+
if !newValue {
86+
resetZoom(animated: false)
87+
}
88+
}
89+
}
90+
}
91+
92+
private func resetZoom(animated: Bool = true) {
93+
if animated {
94+
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
95+
scale = 1.0
96+
offset = .zero
97+
}
98+
} else {
99+
scale = 1.0
100+
offset = .zero
101+
}
102+
lastScale = 1.0
103+
lastOffset = .zero
104+
}
105+
106+
private func clampOffset(geo: GeometryProxy) {
107+
let maxX = max(0, (geo.size.width * (scale - 1)) / 2)
108+
let maxY = max(0, (geo.size.height * (scale - 1)) / 2)
109+
110+
let newX = min(max(offset.width, -maxX), maxX)
111+
let newY = min(max(offset.height, -maxY), maxY)
112+
113+
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
114+
offset = CGSize(width: newX, height: newY)
115+
lastOffset = offset
116+
}
117+
}
118+
}

Neki-iOS/Features/Archive/Sources/Presentation/Sources/View/ArchivePhotoDetailView.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,15 @@ extension ArchivePhotoDetailView {
100100
)
101101
}
102102

103-
// 사진 영역
104103
private var photoTabView: some View {
105104
TabView(selection: $store.currentItemID) {
106105
ForEach(store.photos) { item in
107-
KFImage(item.imageURL)
108-
.resizable()
109-
.placeholder {
110-
ProgressView()
111-
.frame(maxWidth: .infinity, maxHeight: .infinity)
112-
}
113-
.retry(maxCount: 3, interval: .seconds(5))
114-
.cancelOnDisappear(true)
115-
.aspectRatio(contentMode: .fit)
116-
.frame(maxWidth: .infinity, maxHeight: .infinity)
117-
.tag(item.id)
106+
ZoomableImageView(
107+
imageURL: item.imageURL,
108+
isCurrent: store.currentItemID == item.id
109+
)
110+
.frame(maxWidth: .infinity, maxHeight: .infinity)
111+
.tag(item.id)
118112
}
119113
}
120114
.tabViewStyle(.page(indexDisplayMode: .never))

0 commit comments

Comments
 (0)