Skip to content

Commit b5383b3

Browse files
committed
[Feat] #202 - 인스타그램 스토리 공유 기능 구현
1 parent b9646dc commit b5383b3

6 files changed

Lines changed: 99 additions & 19 deletions

File tree

Neki-iOS/Features/Archive/Sources/Presentation/Sources/Feature/ArchivePhotoDetailFeature.swift

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ struct ArchivePhotoDetailFeature {
6363
case toggleDropDownMenu
6464
case closeDropDownMenu
6565

66+
case onTapShareToInstagramStory
67+
case instagramImageFetchResponse(Result<UIImage, Error>)
68+
6669
case delegate(Delegate)
6770
enum Delegate {
6871
case showToast(NekiToastItem)
@@ -175,24 +178,24 @@ struct ArchivePhotoDetailFeature {
175178
switch delegateAction {
176179

177180
case let .didSelectAlbum(albumId):
178-
// let photoId = state.currentItemID
179-
// state.albumSelection = nil
180-
// state.isLoading = true
181-
//
181+
// let photoId = state.currentItemID
182+
// state.albumSelection = nil
183+
// state.isLoading = true
184+
//
182185
// TODO: - 어떤 식으로 업로드 할 건지 서버측과 논의 필요함
183186
// 그냥 킹피셔로 데이터 추출해서 새로운 파일로 업로드하는 것도 가능하긴 한데, 흠
184-
// return .run { send in
185-
// await send(.addToAlbumResponse(Result {
186-
// let isFavorite = albumId == -1
187-
// let targetFolderId = isFavorite ? nil : albumId
188-
//
189-
// try await archiveClient.registerPhotos(
190-
// folderId: targetFolderId,
191-
// uploads: [(mediaID: photoId, memo: String?.none, uploadMethod: PhotoUploadMethod.direct)],
192-
// favorite: isFavorite
193-
// )
194-
// }))
195-
// }
187+
// return .run { send in
188+
// await send(.addToAlbumResponse(Result {
189+
// let isFavorite = albumId == -1
190+
// let targetFolderId = isFavorite ? nil : albumId
191+
//
192+
// try await archiveClient.registerPhotos(
193+
// folderId: targetFolderId,
194+
// uploads: [(mediaID: photoId, memo: String?.none, uploadMethod: PhotoUploadMethod.direct)],
195+
// favorite: isFavorite
196+
// )
197+
// }))
198+
// }
196199
return .none
197200

198201
case .didTapCancel:
@@ -277,6 +280,69 @@ struct ArchivePhotoDetailFeature {
277280
case .deletePhotoResponse(.failure):
278281
return .send(.delegate(.showToast(NekiToastItem("사진을 삭제하지 못했어요", style: .error))))
279282

283+
case .onTapShareToInstagramStory:
284+
state.showDropDownMenu = false
285+
guard let url = state.currentItem?.imageURL else { return .none }
286+
287+
state.isLoading = true
288+
289+
return .run { send in
290+
do {
291+
let image = try await withCheckedThrowingContinuation { continuation in
292+
KingfisherManager.shared.retrieveImage(
293+
with: url,
294+
options: [.cacheOriginalImage]
295+
) { result in
296+
switch result {
297+
case .success(let value):
298+
continuation.resume(returning: value.image)
299+
case .failure(let error):
300+
continuation.resume(throwing: error)
301+
}
302+
}
303+
}
304+
await send(.instagramImageFetchResponse(.success(image)))
305+
} catch {
306+
await send(.instagramImageFetchResponse(.failure(error)))
307+
}
308+
}
309+
310+
case let .instagramImageFetchResponse(.success(image)):
311+
state.isLoading = false
312+
return .run { send in
313+
// 이미지를 클립보드에 담고 인스타그램 앱 열기 (MainActor에서 실행)
314+
let isShared = await MainActor.run { () -> Bool in
315+
guard let urlScheme = URL(string: "instagram-stories://share?source_application=Neki") else { return false }
316+
317+
// 인스타그램 앱이 설치되어 있는지 확인
318+
if UIApplication.shared.canOpenURL(urlScheme) {
319+
if let imageData = image.pngData() {
320+
// 인스타 스토리 배경으로 이미지 전달
321+
let pasteboardItems: [[String: Any]] = [
322+
["com.instagram.sharedSticker.backgroundImage": imageData]
323+
]
324+
let pasteboardOptions = [
325+
UIPasteboard.OptionsKey.expirationDate: Date().addingTimeInterval(60 * 5)
326+
]
327+
328+
UIPasteboard.general.setItems(pasteboardItems, options: pasteboardOptions)
329+
UIApplication.shared.open(urlScheme, options: [:], completionHandler: nil)
330+
return true
331+
}
332+
}
333+
return false
334+
}
335+
336+
// 설치되어 있지 않거나 실패한 경우 토스트
337+
if !isShared {
338+
await send(.delegate(.showToast(NekiToastItem("인스타그램 앱이 설치되어 있지 않아요", style: .error))))
339+
}
340+
}
341+
342+
case .instagramImageFetchResponse(.failure):
343+
state.isLoading = false
344+
return .send(.delegate(.showToast(NekiToastItem("이미지를 불러오지 못했어요", style: .error))))
345+
280346
default:
281347
return .none
282348
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ struct ArchiveAlbumDetailView: View {
5151
}
5252

5353
if store.isLoading {
54-
LoadingView(message: "작업을 수행하고 있어요.")
54+
LoadingView(message: "요청을 처리하고 있어요.")
5555
}
5656
}
5757
.animation(.easeInOut(duration: 0.3), value: store.photos)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct ArchiveAllPhotosView: View {
4545
}
4646

4747
if store.isLoading {
48-
LoadingView(message: "작업을 수행하고 있어요.")
48+
LoadingView(message: "요청을 처리하고 있어요.")
4949
}
5050

5151
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ struct ArchiveFavoriteAlbumView: View {
4747
}
4848

4949
if store.isLoading {
50-
LoadingView(message: "작업을 수행하고 있어요.")
50+
LoadingView(message: "요청을 처리하고 있어요.")
5151
}
5252

5353
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ extension ArchivePhotoDetailView {
159159
.frame(width: 158, height: 34, alignment: .leading)
160160
.padding(.leading, 12)
161161
.contentShape(Rectangle())
162+
163+
Button {
164+
store.send(.onTapShareToInstagramStory)
165+
} label: {
166+
Text("인스타 스토리 공유")
167+
.nekiFont(.body16Medium)
168+
.foregroundStyle(.gray900)
169+
}
170+
.frame(width: 158, height: 34, alignment: .leading)
171+
.padding(.leading, 12)
172+
.contentShape(Rectangle())
162173
#endif
163174

164175
}

Neki-iOS/Info.plist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>CFBundleIdentifier</key>
6+
<string></string>
57
<key>QR_WEBHOOK_URL</key>
68
<string>$(QR_WEBHOOK_URL)</string>
79
<key>BASE_URL</key>
@@ -31,6 +33,7 @@
3133
<string>$(KAKAO_LOGIN_NATIVE_APP_KEY)</string>
3234
<key>LSApplicationQueriesSchemes</key>
3335
<array>
36+
<string>instagram-stories</string>
3437
<string>nmap</string>
3538
<string>kakaomap</string>
3639
<string>comgooglemaps</string>

0 commit comments

Comments
 (0)