@@ -7,20 +7,26 @@ struct MyCollectionDiary: View {
77
88 let diaryId : Int
99 let displayTag : String
10- let onDiaryChanged : ( ( Int ) -> Void ) ?
10+ let onDiaryUpdated : ( ( ) -> Void ) ?
11+ let onDiaryDeleted : ( ( Int ) -> Void ) ?
1112 @StateObject private var viewModel : MyCollectionDiaryViewModel
1213 @State private var isDeleteDialogPresented = false
14+ @State private var keyboardHeight : CGFloat = 0
15+
16+ private let commentFocusAnchorID = " my-collection-diary-comment-focus-anchor "
1317
1418 init (
1519 diaryId: Int ,
1620 displayTag: String ,
1721 diary: DiaryFeedModel ,
18- onDiaryChanged: ( ( Int ) -> Void ) ? = nil ,
22+ onDiaryUpdated: ( ( ) -> Void ) ? = nil ,
23+ onDiaryDeleted: ( ( Int ) -> Void ) ? = nil ,
1924 diaryService: DiaryServicing = DiaryService ( )
2025 ) {
2126 self . diaryId = diaryId
2227 self . displayTag = displayTag
23- self . onDiaryChanged = onDiaryChanged
28+ self . onDiaryUpdated = onDiaryUpdated
29+ self . onDiaryDeleted = onDiaryDeleted
2430 _viewModel = StateObject (
2531 wrappedValue: MyCollectionDiaryViewModel (
2632 diary: diary,
@@ -32,78 +38,101 @@ struct MyCollectionDiary: View {
3238 var body : some View {
3339 GeometryReader { proxy in
3440 let topContentInset = min ( proxy. safeAreaInsets. top, AppSpacing . l) + AppSpacing. s
35- let bottomContentInset = min ( proxy. safeAreaInsets. bottom, AppSpacing . xl) + AppSpacing. l
41+ let keyboardCompensation = keyboardHeight > 0 ? max ( keyboardHeight - 140 , 0 ) : 0
42+ let extraBottomInset = keyboardHeight > 0 ? AppSpacing . s : AppSpacing . l
43+ let bottomContentInset = proxy. safeAreaInsets. bottom + keyboardCompensation + extraBottomInset
3644
3745 ZStack {
3846 Image ( " my_background " )
3947 . resizable ( )
4048 . scaledToFill ( )
4149 . frame ( maxWidth: . infinity, maxHeight: . infinity)
4250 . clipped ( )
43- . ignoresSafeArea ( )
51+ . ignoresSafeArea ( . container , edges : . all )
4452
4553 if viewModel. isDeleted {
4654 MyCollectionDiaryDeletedPlaceholder ( )
4755 } else {
48- ScrollView {
49- VStack ( alignment : . leading , spacing : AppSpacing . m ) {
50- MyCollectionDiaryVideoSection (
51- videoURL : videoURL ,
52- startSeconds : viewModel . startSeconds ,
53- endSeconds : viewModel. endSeconds
54- )
55-
56- MyCollectionDiaryTrackSection (
57- artworkURL : viewModel . diary . albumImageURL ,
58- musicTitle : viewModel. diary. musicTitle ,
59- artist : viewModel. diary. artist ,
60- startMinuteSecondText : viewModel. startMinuteSecondText ,
61- endMinuteSecondText : viewModel. endMinuteSecondText ,
62- startProgress : startProgress ,
63- endProgress : endProgress
64- )
65-
66- MyCollectionDiaryCommentSection (
67- isEditMode : viewModel . isEditMode ,
68- displayedContent : viewModel. displayedContent ,
69- editContentDraft : $ viewModel. editContentDraft ,
70- isProcessing : viewModel. isProcessing ,
71- canSubmitEdit : viewModel. canSubmitEdit ,
72- createdDateText : createdDateText ,
73- tagText : tagText ,
74- isCommentEditorFocused : $isCommentEditorFocused ,
75- onCancelTap : {
76- dismissKeyboard ( )
77- viewModel . cancelEdit ( )
78- } ,
79- onSaveTap : {
80- dismissKeyboard ( )
81- Task {
82- let isSuccess = await viewModel . submitEdit ( )
83- guard isSuccess else { return }
84- }
56+ ScrollViewReader { scrollProxy in
57+ ScrollView {
58+ VStack ( alignment : . leading , spacing : AppSpacing . m ) {
59+ MyCollectionDiaryVideoSection (
60+ videoURL : videoURL ,
61+ startSeconds : viewModel. startSeconds ,
62+ endSeconds : viewModel . endSeconds
63+ )
64+
65+ MyCollectionDiaryTrackSection (
66+ artworkURL : viewModel. diary. albumImageURL ,
67+ musicTitle : viewModel. diary. musicTitle ,
68+ artist : viewModel. diary . artist ,
69+ startMinuteSecondText : viewModel. startMinuteSecondText ,
70+ endMinuteSecondText : viewModel . endMinuteSecondText ,
71+ startProgress : startProgress ,
72+ endProgress : endProgress
73+ )
74+
75+ MyCollectionDiaryCommentSection (
76+ isEditMode : viewModel. isEditMode ,
77+ displayedContent : viewModel. displayedContent ,
78+ editContentDraft : $ viewModel. editContentDraft ,
79+ isProcessing : viewModel. isProcessing ,
80+ canSubmitEdit : viewModel . canSubmitEdit ,
81+ createdDateText : createdDateText ,
82+ tagText : tagText ,
83+ isCommentEditorFocused : $isCommentEditorFocused ,
84+ onCancelTap : handleCancelTap ,
85+ onSaveTap : handleSaveTap
86+ )
87+
88+ if let errorMessage = viewModel . errorMessage {
89+ Text ( errorMessage )
90+ . font ( AppFont . paperlogy4Regular ( size : 13 ) )
91+ . foregroundStyle ( . red . opacity ( 0.95 ) )
92+ . frame ( maxWidth : . infinity , alignment : . leading )
8593 }
86- )
8794
88- if let errorMessage = viewModel. errorMessage {
89- Text ( errorMessage)
90- . font ( AppFont . paperlogy4Regular ( size: 13 ) )
91- . foregroundStyle ( . red. opacity ( 0.95 ) )
92- . frame ( maxWidth: . infinity, alignment: . leading)
95+ Color . clear
96+ . frame ( height: 1 )
97+ . id ( commentFocusAnchorID)
98+ }
99+ . padding ( . horizontal, AppSpacing . l)
100+ . padding ( . top, topContentInset)
101+ . padding ( . bottom, bottomContentInset)
102+ . contentShape ( Rectangle ( ) )
103+ . onTapGesture {
104+ dismissKeyboard ( )
93105 }
94106 }
95- . padding ( . horizontal, AppSpacing . l)
96- . padding ( . top, topContentInset)
97- . padding ( . bottom, bottomContentInset)
98- . contentShape ( Rectangle ( ) )
99- . onTapGesture {
100- dismissKeyboard ( )
107+ . scrollIndicators ( . hidden)
108+ . scrollDismissesKeyboard ( . interactively)
109+ . onChange ( of: isCommentEditorFocused) { isFocused in
110+ guard isFocused else { return }
111+ DispatchQueue . main. async {
112+ withAnimation ( . easeOut( duration: 0.22 ) ) {
113+ scrollProxy. scrollTo ( commentFocusAnchorID, anchor: . bottom)
114+ }
115+ }
116+ }
117+ . onChange ( of: keyboardHeight) { height in
118+ guard isCommentEditorFocused else { return }
119+ guard height > 0 else { return }
120+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.05 ) {
121+ withAnimation ( . easeOut( duration: 0.18 ) ) {
122+ scrollProxy. scrollTo ( commentFocusAnchorID, anchor: . bottom)
123+ }
124+ }
101125 }
102126 }
103- . scrollIndicators ( . hidden)
104127 }
105128 }
106129 }
130+ . onReceive ( NotificationCenter . default. publisher ( for: UIResponder . keyboardWillChangeFrameNotification) ) { notification in
131+ updateKeyboardHeight ( from: notification)
132+ }
133+ . onReceive ( NotificationCenter . default. publisher ( for: UIResponder . keyboardWillHideNotification) ) { notification in
134+ updateKeyboardHeight ( from: notification)
135+ }
107136 . navigationTitle ( " " )
108137 . navigationBarTitleDisplayMode ( . inline)
109138 . toolbar ( . visible, for: . navigationBar)
@@ -137,7 +166,7 @@ struct MyCollectionDiary: View {
137166 Task {
138167 let isSuccess = await viewModel. deleteDiary ( )
139168 if isSuccess {
140- onDiaryChanged ? ( diaryId)
169+ onDiaryDeleted ? ( diaryId)
141170 dismiss ( )
142171 }
143172 }
@@ -188,6 +217,50 @@ struct MyCollectionDiary: View {
188217 return raw. hasPrefix ( " @ " ) ? raw : " @ \( raw) "
189218 }
190219
220+ private func updateKeyboardHeight( from notification: Notification ) {
221+ guard
222+ let userInfo = notification. userInfo,
223+ let endFrame = userInfo [ UIResponder . keyboardFrameEndUserInfoKey] as? CGRect
224+ else {
225+ return
226+ }
227+
228+ let keyWindow = UIApplication . shared. connectedScenes
229+ . compactMap { $0 as? UIWindowScene }
230+ . flatMap ( \. windows)
231+ . first ( where: \. isKeyWindow)
232+
233+ let overlapHeight : CGFloat
234+ if let keyWindow {
235+ let endFrameInWindow = keyWindow. convert ( endFrame, from: nil )
236+ overlapHeight = max (
237+ 0 ,
238+ keyWindow. bounds. maxY - endFrameInWindow. minY - keyWindow. safeAreaInsets. bottom
239+ )
240+ } else {
241+ overlapHeight = max ( 0 , UIScreen . main. bounds. maxY - endFrame. minY)
242+ }
243+
244+ let duration = ( userInfo [ UIResponder . keyboardAnimationDurationUserInfoKey] as? Double ) ?? 0.25
245+ withAnimation ( . easeOut( duration: duration) ) {
246+ keyboardHeight = overlapHeight
247+ }
248+ }
249+
250+ private func handleCancelTap( ) {
251+ dismissKeyboard ( )
252+ viewModel. cancelEdit ( )
253+ }
254+
255+ private func handleSaveTap( ) {
256+ dismissKeyboard ( )
257+ Task {
258+ let isSuccess = await viewModel. submitEdit ( )
259+ guard isSuccess else { return }
260+ onDiaryUpdated ? ( )
261+ }
262+ }
263+
191264 private func dismissKeyboard( ) {
192265 isCommentEditorFocused = false
193266 UIApplication . shared. sendAction (
0 commit comments