From 066e27fcfde79d5f1087c61501e1420bb4a8ec82 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 22 Sep 2026 15:29:03 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20UIKitTextEditor=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=8B=A4=EC=A4=91=20=EC=9E=85=EB=A0=A5=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Goal/Create/GoalCreateView.swift | 21 ++++++++-- .../Record/Editor/RecordEditorView.swift | 21 ++++++++-- .../Common/Component/UIKitTextEditor.swift | 42 ++++++++++--------- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift index cef89aa8..cd6caca6 100644 --- a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift +++ b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift @@ -144,10 +144,25 @@ private struct GoalCreateDescriptionEditor: View { Group { switch store.selectedTab { case .write: - TextEditor(text: $store.markdownContent) + UIKitTextEditor() + .composable( + update: { textEditor in + textEditor.updateInput( + text: $store.markdownContent, + isFocused: focusedField == .content, + onFocusChange: { isFocused in + if isFocused { + focusedField = .content + } else if focusedField == .content { + focusedField = nil + } + }, + isEnabled: !store.isSaving + ) + }, + sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + ) .focused($focusedField, equals: .content) - .font(.body) - .scrollContentBackground(.hidden) .padding(12) case .preview: Group { diff --git a/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift b/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift index 51cf7c8c..2e0805d6 100644 --- a/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift +++ b/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift @@ -138,10 +138,25 @@ public struct RecordEditorView: View { switch store.selectedTab { case .write: - TextEditor(text: $store.markdownContent) + UIKitTextEditor() + .composable( + update: { textEditor in + textEditor.updateInput( + text: $store.markdownContent, + isFocused: focusedField == .content, + onFocusChange: { isFocused in + if isFocused { + focusedField = .content + } else if focusedField == .content { + focusedField = nil + } + }, + isEnabled: !store.isLoading + ) + }, + sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + ) .focused($focusedField, equals: .content) - .font(.body) - .scrollContentBackground(.hidden) .padding(12) .frame(minHeight: 340, alignment: .topLeading) .background(Color.surfaceSecondary, in: .rect(cornerRadius: 16)) diff --git a/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift b/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift index 259c2a91..5da09654 100644 --- a/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift +++ b/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift @@ -8,46 +8,49 @@ import SwiftUI import UIComposable -final class UIKitTextEditor: UITextView, UICoordinatedComposable { - var textBinding: Binding? - var isEditorFocused = false - var onFocusChange: ((Bool) -> Void)? - var placeholder = "" +public final class UIKitTextEditor: UITextView, UICoordinatedComposable { + private var textBinding: Binding? + private var isEditorFocused = false + private var onFocusChange: ((Bool) -> Void)? + private var placeholder: String? - func makeCoordinator() -> Coordinator { + public func makeCoordinator() -> Coordinator { Coordinator() } - func connect(coordinator: Coordinator) { + public func connect(coordinator: Coordinator) { delegate = coordinator coordinator.textView = self configureAppearance() } - func update(coordinator: Coordinator) { + public func update(coordinator: Coordinator) { coordinator.update() } - func disconnect(coordinator: Coordinator) { + public func disconnect(coordinator: Coordinator) { if delegate === coordinator { delegate = nil } coordinator.disconnect() } - func updateInput( + public func updateInput( text: Binding, isFocused: Bool, onFocusChange: @escaping (Bool) -> Void, - placeholder: String + placeholder: String? = nil, + isEnabled: Bool = true ) { textBinding = text isEditorFocused = isFocused self.onFocusChange = onFocusChange self.placeholder = placeholder + isEditable = isEnabled + isSelectable = isEnabled } - static func fittingSize( + public static func fittingSize( proposal: ProposedViewSize, textEditor: UIKitTextEditor ) -> CGSize? { @@ -74,7 +77,7 @@ final class UIKitTextEditor: UITextView, UICoordinatedComposable { setContentHuggingPriority(.defaultLow, for: .horizontal) } - final class Coordinator: NSObject, UITextViewDelegate { + public final class Coordinator: NSObject, UITextViewDelegate { weak var textView: UIKitTextEditor? private weak var scrollView: UIScrollView? private var offsetObservation: NSKeyValueObservation? @@ -109,12 +112,12 @@ final class UIKitTextEditor: UITextView, UICoordinatedComposable { textView = nil } - func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { startTrackingOffset(for: textView) return true } - func textViewDidBeginEditing(_ textView: UITextView) { + public func textViewDidBeginEditing(_ textView: UITextView) { guard let textView = textView as? UIKitTextEditor else { return } if isShowingPlaceholder(in: textView) { @@ -134,14 +137,14 @@ final class UIKitTextEditor: UITextView, UICoordinatedComposable { } } - func textViewDidChange(_ textView: UITextView) { + public func textViewDidChange(_ textView: UITextView) { guard let textView = textView as? UIKitTextEditor else { return } stopTrackingOffset() textView.textBinding?.wrappedValue = textView.text } - func textViewDidEndEditing(_ textView: UITextView) { + public func textViewDidEndEditing(_ textView: UITextView) { guard let textView = textView as? UIKitTextEditor else { return } if textView.isEditorFocused { @@ -154,10 +157,11 @@ final class UIKitTextEditor: UITextView, UICoordinatedComposable { } func applyPlaceholderIfNeeded(to textView: UIKitTextEditor) { - guard let textBinding = textView.textBinding else { return } + guard let textBinding = textView.textBinding, + let placeholder = textView.placeholder else { return } if textBinding.wrappedValue.isEmpty && !textView.isFirstResponder { - textView.text = textView.placeholder + textView.text = placeholder textView.textColor = .placeholderText } else if isShowingPlaceholder(in: textView) { textView.text = textBinding.wrappedValue From e563eb54567319348a3d2518b61259b42a09759c Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 22 Sep 2026 16:28:46 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20GoalCreateView=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20Binding=20=EC=A0=84=EB=8B=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Goal/Create/GoalCreateView.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift index cd6caca6..6ca0aaa3 100644 --- a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift +++ b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift @@ -32,8 +32,8 @@ public struct GoalCreateView: View { NavigationStack { ScrollView { LazyVStack(alignment: .leading, spacing: 24) { - GoalCreateTitleField(store: store, focusedField: _focusedField) - GoalCreateDescriptionEditor(store: store, focusedField: _focusedField) + GoalCreateTitleField(store: store, focusedField: $focusedField) + GoalCreateDescriptionEditor(store: store, focusedField: $focusedField) GoalCreateStatusField() GoalCreateTodoField(store: store) } @@ -109,7 +109,7 @@ private struct GoalCreateTopBar: View { private struct GoalCreateTitleField: View { @Bindable var store: StoreOf - @FocusState var focusedField: GoalCreateField? + let focusedField: FocusState.Binding var body: some View { VStack(alignment: .leading, spacing: 14) { @@ -120,7 +120,7 @@ private struct GoalCreateTitleField: View { RecordPresentation.text("development_goal_create_title_placeholder"), text: $store.title ) - .focused($focusedField, equals: .title) + .focused(focusedField, equals: .title) .font(.body) .padding() .background(Color.surface, in: .rect(cornerRadius: 24)) @@ -131,7 +131,7 @@ private struct GoalCreateTitleField: View { private struct GoalCreateDescriptionEditor: View { @Bindable var store: StoreOf - @FocusState var focusedField: GoalCreateField? + let focusedField: FocusState.Binding var body: some View { VStack(alignment: .leading, spacing: 14) { @@ -139,7 +139,7 @@ private struct GoalCreateDescriptionEditor: View { .font(.headline) VStack(spacing: 16) { - GoalCreateModePicker(store: store, focusedField: _focusedField) + GoalCreateModePicker(store: store, focusedField: focusedField) Group { switch store.selectedTab { @@ -149,12 +149,12 @@ private struct GoalCreateDescriptionEditor: View { update: { textEditor in textEditor.updateInput( text: $store.markdownContent, - isFocused: focusedField == .content, + isFocused: focusedField.wrappedValue == .content, onFocusChange: { isFocused in if isFocused { - focusedField = .content - } else if focusedField == .content { - focusedField = nil + focusedField.wrappedValue = .content + } else if focusedField.wrappedValue == .content { + focusedField.wrappedValue = nil } }, isEnabled: !store.isSaving @@ -162,7 +162,7 @@ private struct GoalCreateDescriptionEditor: View { }, sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } ) - .focused($focusedField, equals: .content) + .focused(focusedField, equals: .content) .padding(12) case .preview: Group { @@ -199,7 +199,7 @@ private struct GoalCreateDescriptionEditor: View { private struct GoalCreateModePicker: View { @Bindable var store: StoreOf - @FocusState var focusedField: GoalCreateField? + let focusedField: FocusState.Binding var body: some View { HStack(spacing: 0) { @@ -208,13 +208,13 @@ private struct GoalCreateModePicker: View { isSelected: store.selectedTab == .write ) { store.send(.binding(.set(\.selectedTab, .write))) - focusedField = .content + focusedField.wrappedValue = .content } GoalCreateModeButton( title: RecordPresentation.text("development_record_preview"), isSelected: store.selectedTab == .preview ) { - focusedField = nil + focusedField.wrappedValue = nil store.send(.binding(.set(\.selectedTab, .preview))) } } From 15708130ef9ee7c487688cd3594119b3df6f9896 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 22 Sep 2026 16:29:14 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20UIKitTextEditor=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A7=80=EC=A0=95=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Goal/Create/GoalCreateView.swift | 3 +- .../Record/Editor/RecordEditorView.swift | 3 +- .../Common/Component/UIKitTextEditor.swift | 12 ------- .../Sources/Todo/Editor/TodoEditorView.swift | 35 +++++++++---------- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift index 6ca0aaa3..58ae87c3 100644 --- a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift +++ b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift @@ -159,8 +159,7 @@ private struct GoalCreateDescriptionEditor: View { }, isEnabled: !store.isSaving ) - }, - sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + } ) .focused(focusedField, equals: .content) .padding(12) diff --git a/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift b/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift index 2e0805d6..6c10ff52 100644 --- a/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift +++ b/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift @@ -153,8 +153,7 @@ public struct RecordEditorView: View { }, isEnabled: !store.isLoading ) - }, - sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + } ) .focused($focusedField, equals: .content) .padding(12) diff --git a/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift b/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift index 5da09654..98687e01 100644 --- a/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift +++ b/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift @@ -50,18 +50,6 @@ public final class UIKitTextEditor: UITextView, UICoordinatedComposable { isSelectable = isEnabled } - public static func fittingSize( - proposal: ProposedViewSize, - textEditor: UIKitTextEditor - ) -> CGSize? { - guard let width = proposal.width else { return nil } - - let size = textEditor.sizeThatFits( - CGSize(width: width, height: .greatestFiniteMagnitude) - ) - return CGSize(width: width, height: size.height) - } - private func configureAppearance() { font = UIFont.preferredFont(forTextStyle: .body) backgroundColor = .clear diff --git a/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift b/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift index c40217d6..9253df61 100644 --- a/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift +++ b/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift @@ -335,26 +335,23 @@ private struct ContentView: View { VStack(alignment: .leading, spacing: 8) { markdownHint UIKitTextEditor() - .composable( - update: { textEditor in - textEditor.updateInput( - text: $store.content, - isFocused: isContentFocused, - onFocusChange: { isFocused in - if isFocused { - field = .content - } else if field == .content { - field = nil - } - }, - placeholder: String( - localized: "todo_editor_description_optional", - bundle: PresentationResources.bundle - ) + .composable { textEditor in + textEditor.updateInput( + text: $store.content, + isFocused: isContentFocused, + onFocusChange: { isFocused in + if isFocused { + field = .content + } else if field == .content { + field = nil + } + }, + placeholder: String( + localized: "todo_editor_description_optional", + bundle: PresentationResources.bundle ) - }, - sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } - ) + ) + } // SwiftUI 포커싱 시스템에 등록하기 위해 사용 .focused($field, equals: .content) } From 39974dcd5a2c84e2401b46e4db8a64dc60a4090b Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 22 Sep 2026 17:03:49 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EA=B3=B5=ED=86=B5=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EC=98=81=EC=97=AD=20=EC=B5=9C=EC=86=8C=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=EC=99=80=20=EB=82=B4=EC=9A=A9=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Goal/Create/GoalCreateView.swift | 53 +++++++++-------- .../Record/Editor/RecordEditorView.swift | 54 +++++++++-------- .../Common/Component/UIKitTextEditor.swift | 58 +++++++++++++++++++ .../Sources/Todo/Editor/TodoEditorView.swift | 37 ++++++------ 4 files changed, 135 insertions(+), 67 deletions(-) diff --git a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift index 58ae87c3..e9326709 100644 --- a/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift +++ b/Application/Presentation/Development/Sources/Goal/Create/GoalCreateView.swift @@ -144,25 +144,33 @@ private struct GoalCreateDescriptionEditor: View { Group { switch store.selectedTab { case .write: - UIKitTextEditor() - .composable( - update: { textEditor in - textEditor.updateInput( - text: $store.markdownContent, - isFocused: focusedField.wrappedValue == .content, - onFocusChange: { isFocused in - if isFocused { - focusedField.wrappedValue = .content - } else if focusedField.wrappedValue == .content { - focusedField.wrappedValue = nil - } - }, - isEnabled: !store.isSaving - ) - } - ) - .focused(focusedField, equals: .content) - .padding(12) + TextEditorContentLayout(minimumHeight: 120 - 24) { + Text(RecordPresentation.text("development_goal_create_markdown_hint")) + .font(.caption) + .foregroundStyle(Color.textTertiary) + UIKitTextEditor() + .composable( + update: { textEditor in + textEditor.updateInput( + text: $store.markdownContent, + isFocused: focusedField.wrappedValue == .content, + onFocusChange: { isFocused in + if isFocused { + focusedField.wrappedValue = .content + } else if focusedField.wrappedValue == .content { + focusedField.wrappedValue = nil + } + }, + isEnabled: !store.isSaving + ) + }, + sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + ) + .focused(focusedField, equals: .content) + } + .padding(12) + .contentShape(.rect) + .onTapGesture { focusedField.wrappedValue = .content } case .preview: Group { if store.markdownContent.isEmpty { @@ -181,13 +189,8 @@ private struct GoalCreateDescriptionEditor: View { } } } - .frame(minHeight: 120) + .frame(minHeight: 120, alignment: .topLeading) .background(Color.surfaceSecondary, in: .rect(cornerRadius: 16)) - - Text(RecordPresentation.text("development_goal_create_markdown_hint")) - .font(.caption) - .foregroundStyle(Color.textTertiary) - .frame(maxWidth: .infinity, alignment: .leading) } .padding(16) .background(Color.surface, in: .rect(cornerRadius: 24)) diff --git a/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift b/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift index 6c10ff52..ca8fbe2e 100644 --- a/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift +++ b/Application/Presentation/Development/Sources/Record/Editor/RecordEditorView.swift @@ -138,27 +138,35 @@ public struct RecordEditorView: View { switch store.selectedTab { case .write: - UIKitTextEditor() - .composable( - update: { textEditor in - textEditor.updateInput( - text: $store.markdownContent, - isFocused: focusedField == .content, - onFocusChange: { isFocused in - if isFocused { - focusedField = .content - } else if focusedField == .content { - focusedField = nil - } - }, - isEnabled: !store.isLoading - ) - } - ) - .focused($focusedField, equals: .content) - .padding(12) - .frame(minHeight: 340, alignment: .topLeading) - .background(Color.surfaceSecondary, in: .rect(cornerRadius: 16)) + TextEditorContentLayout(minimumHeight: 340 - 24) { + Text(RecordPresentation.text("development_record_markdown_hint")) + .font(.caption) + .foregroundStyle(Color.textTertiary) + UIKitTextEditor() + .composable( + update: { textEditor in + textEditor.updateInput( + text: $store.markdownContent, + isFocused: focusedField == .content, + onFocusChange: { isFocused in + if isFocused { + focusedField = .content + } else if focusedField == .content { + focusedField = nil + } + }, + isEnabled: !store.isLoading + ) + }, + sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + ) + .focused($focusedField, equals: .content) + } + .padding(12) + .frame(minHeight: 340, alignment: .topLeading) + .background(Color.surfaceSecondary, in: .rect(cornerRadius: 16)) + .contentShape(.rect) + .onTapGesture { focusedField = .content } case .preview: Group { if store.markdownContent.isEmpty { @@ -178,10 +186,6 @@ public struct RecordEditorView: View { .background(Color.surfaceSecondary, in: .rect(cornerRadius: 16)) } - Text(RecordPresentation.text("development_record_markdown_hint")) - .font(.caption) - .foregroundStyle(Color.textTertiary) - .frame(maxWidth: .infinity, alignment: .leading) } .padding(20) .background { diff --git a/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift b/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift index 98687e01..17ce98d5 100644 --- a/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift +++ b/Application/Presentation/PresentationShared/Sources/Common/Component/UIKitTextEditor.swift @@ -50,6 +50,20 @@ public final class UIKitTextEditor: UITextView, UICoordinatedComposable { isSelectable = isEnabled } + public static func fittingSize( + proposal: ProposedViewSize, + textEditor: UIKitTextEditor + ) -> CGSize? { + guard let width = proposal.width, width.isFinite, 0 < width else { return nil } + + let size = textEditor.sizeThatFits( + CGSize(width: width, height: .greatestFiniteMagnitude) + ) + let proposedHeight = proposal.height ?? 0 + let height = proposedHeight.isFinite ? max(size.height, proposedHeight) : size.height + return CGSize(width: width, height: height) + } + private func configureAppearance() { font = UIFont.preferredFont(forTextStyle: .body) backgroundColor = .clear @@ -223,6 +237,50 @@ public final class UIKitTextEditor: UITextView, UICoordinatedComposable { } } +/// 안내 문구와 편집기를 순서대로 배치하고 남은 최소 높이를 편집기에 제안합니다. +public struct TextEditorContentLayout: Layout { + private let minimumHeight: CGFloat + private let spacing = CGFloat(8) + + public init(minimumHeight: CGFloat) { + self.minimumHeight = max(0, minimumHeight) + } + + public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + guard subviews.count == 2 else { return .zero } + + let width = proposal.width.flatMap { $0.isFinite ? $0 : nil } + ?? subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0 + let hintSize = subviews[0].sizeThatFits(ProposedViewSize(width: width, height: nil)) + let editorSize = subviews[1].sizeThatFits(ProposedViewSize( + width: width, + height: max(0, minimumHeight - hintSize.height - spacing) + )) + + return CGSize(width: width, height: max(minimumHeight, hintSize.height + spacing + editorSize.height)) + } + + public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + guard subviews.count == 2 else { return } + + let hintProposal = ProposedViewSize(width: bounds.width, height: nil) + let hintSize = subviews[0].sizeThatFits(hintProposal) + subviews[0].place( + at: CGPoint(x: bounds.minX, y: bounds.minY), + anchor: .topLeading, + proposal: hintProposal + ) + subviews[1].place( + at: CGPoint(x: bounds.minX, y: bounds.minY + hintSize.height + spacing), + anchor: .topLeading, + proposal: ProposedViewSize( + width: bounds.width, + height: max(0, bounds.height - hintSize.height - spacing) + ) + ) + } +} + private extension UIView { var enclosingScrollView: UIScrollView? { var currentSuperview = superview diff --git a/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift b/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift index 9253df61..8f4656c9 100644 --- a/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift +++ b/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift @@ -332,26 +332,29 @@ private struct ContentView: View { let isContentFocused = field == .content Group { if store.tabViewTag == .editor { - VStack(alignment: .leading, spacing: 8) { + TextEditorContentLayout(minimumHeight: max(0, minimumHeight - 20)) { markdownHint UIKitTextEditor() - .composable { textEditor in - textEditor.updateInput( - text: $store.content, - isFocused: isContentFocused, - onFocusChange: { isFocused in - if isFocused { - field = .content - } else if field == .content { - field = nil - } - }, - placeholder: String( - localized: "todo_editor_description_optional", - bundle: PresentationResources.bundle + .composable( + update: { textEditor in + textEditor.updateInput( + text: $store.content, + isFocused: isContentFocused, + onFocusChange: { isFocused in + if isFocused { + field = .content + } else if field == .content { + field = nil + } + }, + placeholder: String( + localized: "todo_editor_description_optional", + bundle: PresentationResources.bundle + ) ) - ) - } + }, + sizeThatFits: { UIKitTextEditor.fittingSize(proposal: $0, textEditor: $1) } + ) // SwiftUI 포커싱 시스템에 등록하기 위해 사용 .focused($field, equals: .content) }