From bf997e57381378b06e067966694e95000cd15d05 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 20 Sep 2026 18:09:53 +0900 Subject: [PATCH 1/5] =?UTF-8?q?ui:=20Home=20=EA=B0=9C=EB=B0=9C=20=EB=AA=A9?= =?UTF-8?q?=ED=91=9C=20=EC=A7=84=EC=9E=85=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeTab/Sources/Home/HomeView.swift | 173 +++++++++++++----- .../Resources/Localizable.xcstrings | 17 ++ 2 files changed, 140 insertions(+), 50 deletions(-) diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift index 2f8469b1..13b277f4 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift @@ -11,9 +11,12 @@ import Domain import PresentationShared public struct HomeView: View { + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dynamicTypeSize) private var dynamicTypeSize @Environment(\.openWindow) private var openWindow @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) + @ScaledMetric(relativeTo: .title2) private var categoryIconSize = CGFloat(48) @State private var path = [HomeRoute]() @State private var searchStore: StoreOf @State private var store: StoreOf @@ -41,14 +44,21 @@ public struct HomeView: View { public var body: some View { NavigationStack(path: $path) { - List { - todoSection + ScrollView { + LazyVStack(alignment: .leading, spacing: 16, pinnedViews: [.sectionHeaders]) { + Section { + todoSection + } header: { + topBar + } + } + .padding(.horizontal, 16) } - .listStyle(.insetGrouped) - .navigationTitle(String(localized: "nav_home", bundle: PresentationResources.bundle)) + .background(Color.appBackground) + .toolbarVisibility(.hidden, for: .navigationBar) .navigationDestination(for: HomeRoute.self, destination: destinationView) - .toolbar { toolbar } } + .toolbarBackground(Color.appBackground) .onAppear { store.send(.view(.startObserving)) } .onChange(of: isSelected, initial: true) { _, isSelected in if isSelected { @@ -73,56 +83,91 @@ public struct HomeView: View { ) } - private var todoSection: some View { - Section(content: { - if store.isPreferencesLoading { - LoadingView() - } else { - let preferences = store.preferences - ForEach(preferences.filter { $0.isVisible }, id: \.id) { item in - todoCategoryRow(item) - .listRowInsets((EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))) - } + private var topBar: some View { + HStack(spacing: 12) { + Text("DevLog") + .font(.largeTitle.weight(.bold)) + Spacer() + Button { + store.send(.store(.setPresentation(.searchView, true))) + } label: { + Image(systemName: "magnifyingglass") + .font(.title3.weight(.semibold)) + .frame(width: 28, height: 28) + } + .adaptiveButtonStyle( + shape: .circle, + color: .surface, + glassEffect: .enabled + ) + Button { + store.send(.store(.setPresentation(.contentPicker, true))) + } label: { + Image(systemName: "plus") + .font(.title3.weight(.semibold)) + .frame(width: 28, height: 28) } - }, header: { - HStack { + .adaptiveButtonStyle( + shape: .circle, + color: .surface, + glassEffect: .enabled + ) + .disabled(!store.isNetworkConnected) + } + .padding(.bottom, 8) + .background(Color.appBackground) + } + + private var todoSection: some View { + VStack(alignment: .leading, spacing: 24) { + HStack(spacing: 8) { Text("TODO", bundle: PresentationResources.bundle) - .foregroundStyle(Color.primary) - .font(.title2) - .bold() + .font(.title3.weight(.semibold)) + .foregroundStyle(.primary) + Text("\(store.preferences.filter(\.isVisible).count)") + .font(.caption.weight(.medium)) + .foregroundStyle(Color.accent) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(categoryCountBackground, in: Capsule()) Spacer() - Button(action: { + Button { store.send(.view(.tapManageTodoCategory)) - }) { + } label: { Image(systemName: "ellipsis") - .font(.title2) - .foregroundStyle(Color.gray) + .font(.title3.weight(.semibold)) + .foregroundStyle(Color.textTertiary) + .frame(width: 32, height: 32) } } - .listRowInsets(EdgeInsets()) // 헤더의 padding 제거 - }) - } - @ToolbarContentBuilder - private var toolbar: some ToolbarContent { - ToolbarItem(placement: .topBarTrailing) { - Button { - store.send(.store(.setPresentation(.contentPicker, true))) - } label: { - Image(systemName: "plus") + if store.isPreferencesLoading { + LoadingView() + .frame(maxWidth: .infinity) + } else { + LazyVGrid( + columns: Array( + repeating: GridItem(.flexible(), spacing: 12), + count: dynamicTypeSize.isAccessibilitySize ? 2 : 4 + ), + spacing: 20 + ) { + ForEach(store.preferences.filter(\.isVisible)) { item in + todoCategoryRow(item) + } + } } - .disabled(!store.isNetworkConnected) - } - if #available(iOS 26.0, *) { - ToolbarSpacer(.fixed, placement: .topBarTrailing) } - ToolbarItemGroup(placement: .topBarTrailing) { - Button { - store.send(.store(.setPresentation(.searchView, true))) - } label: { - Image(systemName: "magnifyingglass") - } + .padding(16) + .background(Color.surface, in: RoundedRectangle(cornerRadius: 28)) + } + + private var categoryCountBackground: Color { + if colorScheme == .dark { + return .surfaceSecondary } + + return Color.accent.opacity(0.12) } @ViewBuilder @@ -211,15 +256,28 @@ public struct HomeView: View { } } - @ViewBuilder private func todoCategoryRow(_ item: TodoCategoryItem) -> some View { NavigationLink(value: HomeRoute.category(item)) { - labelImage( - text: item.localizedName, - systemName: item.symbolName, - imageColor: item.color - ) + VStack(spacing: 10) { + RoundedRectangle(cornerRadius: 20) + .fill(categoryIconBackground(item)) + .frame(width: categoryIconSize, height: categoryIconSize) + .overlay { + Image(systemName: item.symbolName) + .font(.title3.bold()) + .foregroundStyle(categoryIconForeground(item)) + } + Text(item.localizedName) + .font(.subheadline) + .foregroundStyle(Color.textSecondary) + .lineLimit(2) + .multilineTextAlignment(.center) + .minimumScaleFactor(0.8) + } + .frame(maxWidth: .infinity) + .contentShape(.rect) } + .buttonStyle(.plain) } private func labelImage( @@ -255,6 +313,21 @@ public struct HomeView: View { } } + private func categoryIconBackground(_ item: TodoCategoryItem) -> Color { + if colorScheme == .dark { + return item.color + } + + return item.color.opacity(0.12) + } + + private func categoryIconForeground(_ item: TodoCategoryItem) -> Color { + if colorScheme == .dark { + return .white + } + + return item.color + } } public enum HomeRoute: Hashable { diff --git a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings index fdbe50ef..aede72b2 100644 --- a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings +++ b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings @@ -3200,6 +3200,23 @@ } } }, + "todo_category_manage" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage Categories" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "카테고리 관리" + } + } + } + }, "todo_category_doc" : { "comment" : "Todo category: Documentation", "extractionState" : "manual", From f4a00254961c8b4016f2a66d584c8f5353177a30 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 20 Sep 2026 18:54:00 +0900 Subject: [PATCH 2/5] =?UTF-8?q?ui:=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeTab/Sources/Home/HomeView.swift | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift index 13b277f4..d57a7ef3 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift @@ -17,6 +17,7 @@ public struct HomeView: View { @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) @ScaledMetric(relativeTo: .title2) private var categoryIconSize = CGFloat(48) + @State private var isTodoCategoryExpanded = false @State private var path = [HomeRoute]() @State private var searchStore: StoreOf @State private var store: StoreOf @@ -119,28 +120,7 @@ public struct HomeView: View { } private var todoSection: some View { - VStack(alignment: .leading, spacing: 24) { - HStack(spacing: 8) { - Text("TODO", bundle: PresentationResources.bundle) - .font(.title3.weight(.semibold)) - .foregroundStyle(.primary) - Text("\(store.preferences.filter(\.isVisible).count)") - .font(.caption.weight(.medium)) - .foregroundStyle(Color.accent) - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background(categoryCountBackground, in: Capsule()) - Spacer() - Button { - store.send(.view(.tapManageTodoCategory)) - } label: { - Image(systemName: "ellipsis") - .font(.title3.weight(.semibold)) - .foregroundStyle(Color.textTertiary) - .frame(width: 32, height: 32) - } - } - + VStack(alignment: .leading, spacing: 0) { if store.isPreferencesLoading { LoadingView() .frame(maxWidth: .infinity) @@ -152,22 +132,63 @@ public struct HomeView: View { ), spacing: 20 ) { - ForEach(store.preferences.filter(\.isVisible)) { item in + let preferences = store.preferences.filter(\.isVisible) + let visiblePreferences = isTodoCategoryExpanded + ? preferences + : Array(preferences.prefix(8)) + ForEach(visiblePreferences) { item in todoCategoryRow(item) } + if isTodoCategoryExpanded { + addTodoCategoryButton + } } } + + HStack { + Spacer() + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isTodoCategoryExpanded.toggle() + } + } label: { + Image(systemName: isTodoCategoryExpanded ? "chevron.up" : "chevron.down") + .font(.title3.weight(.semibold)) + .foregroundStyle(Color.textTertiary) + .frame(width: 32, height: 32) + } + Spacer() + } } - .padding(16) + .padding(.top, 20) + .padding(.bottom, 12) .background(Color.surface, in: RoundedRectangle(cornerRadius: 28)) } - private var categoryCountBackground: Color { - if colorScheme == .dark { - return .surfaceSecondary + private var addTodoCategoryButton: some View { + Button { + store.send(.view(.tapManageTodoCategory)) + } label: { + VStack(spacing: 10) { + RoundedRectangle(cornerRadius: 20) + .strokeBorder( + Color.textTertiary, + style: StrokeStyle(lineWidth: 1.5, dash: [5, 4]) + ) + .frame(width: categoryIconSize, height: categoryIconSize) + .overlay { + Image(systemName: "plus") + .font(.title3.bold()) + .foregroundStyle(Color.textTertiary) + } + Text("todo_add", bundle: PresentationResources.bundle) + .font(.subheadline) + .foregroundStyle(Color.textTertiary) + } + .frame(maxWidth: .infinity) + .contentShape(.rect) } - - return Color.accent.opacity(0.12) + .buttonStyle(.plain) } @ViewBuilder From 9d8642edbfee21ab80ac328ae1911ca2fff55893 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 20 Sep 2026 19:49:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20Store=EC=97=90=20expand=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeTab/Sources/Home/HomeFeature.swift | 4 ++++ .../HomeTab/Sources/Home/HomeView.swift | 14 +++++++------- .../Tests/Home/HomeFeatureTestSupport.swift | 5 +++++ .../HomeTab/Tests/Home/HomeFeatureTests.swift | 9 +++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeFeature.swift b/Application/Presentation/HomeTab/Sources/Home/HomeFeature.swift index 5837d661..20a646ce 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeFeature.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeFeature.swift @@ -17,6 +17,7 @@ struct HomeFeature { @Presents var sheet: SheetState? @Presents var fullScreenCover: FullScreenCoverState? var preferences = [TodoCategoryItem]() + var isTodoCategoryExpanded = false var isNetworkConnected = true var selectedTodoCategory: TodoCategory? var loading = LoadingFeature.State() @@ -52,6 +53,7 @@ struct HomeFeature { case fetchData case todoEditorCreated case tapManageTodoCategory + case tapTodoCategoryExpansionButton case tapTodoCategory(TodoCategory) } @@ -211,6 +213,8 @@ private extension HomeFeature { ) case .tapManageTodoCategory: state.sheet = .reorderTodo(CategoryManageFeature.State(preferences: state.preferences)) + case .tapTodoCategoryExpansionButton: + state.isTodoCategoryExpanded.toggle() case .tapTodoCategory(let category): state.selectedTodoCategory = category state.sheet = nil diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift index d57a7ef3..8148a5f8 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift @@ -17,7 +17,6 @@ public struct HomeView: View { @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) @ScaledMetric(relativeTo: .title2) private var categoryIconSize = CGFloat(48) - @State private var isTodoCategoryExpanded = false @State private var path = [HomeRoute]() @State private var searchStore: StoreOf @State private var store: StoreOf @@ -133,13 +132,13 @@ public struct HomeView: View { spacing: 20 ) { let preferences = store.preferences.filter(\.isVisible) - let visiblePreferences = isTodoCategoryExpanded + let visiblePreferences = store.isTodoCategoryExpanded ? preferences : Array(preferences.prefix(8)) ForEach(visiblePreferences) { item in todoCategoryRow(item) } - if isTodoCategoryExpanded { + if store.isTodoCategoryExpanded { addTodoCategoryButton } } @@ -148,11 +147,12 @@ public struct HomeView: View { HStack { Spacer() Button { - withAnimation(.easeInOut(duration: 0.2)) { - isTodoCategoryExpanded.toggle() - } + store.send( + .view(.tapTodoCategoryExpansionButton), + animation: .easeInOut(duration: 0.2) + ) } label: { - Image(systemName: isTodoCategoryExpanded ? "chevron.up" : "chevron.down") + Image(systemName: store.isTodoCategoryExpanded ? "chevron.up" : "chevron.down") .font(.title3.weight(.semibold)) .foregroundStyle(Color.textTertiary) .frame(width: 32, height: 32) diff --git a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift index 5686cdd1..007850a7 100644 --- a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift +++ b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift @@ -19,6 +19,7 @@ struct HomeStoreTestAdapter { private let clock: TestClock var preferences: [TodoCategoryItem] { store.state.preferences } + var isTodoCategoryExpanded: Bool { store.state.isTodoCategoryExpanded } var isNetworkConnected: Bool { store.state.isNetworkConnected } var showContentPicker: Bool { store.state.showContentPicker } var showCategoryManage: Bool { @@ -78,6 +79,10 @@ struct HomeStoreTestAdapter { await drainReceivedActions() } + func tapTodoCategoryExpansionButton() async { + await store.send(.view(.tapTodoCategoryExpansionButton)) + } + func orderTodoCategory(_ items: [TodoCategoryItem]) async { await store.send(.sheet(.presented(.categoryManage(.delegate(.done(items)))))) await drainReceivedActions() diff --git a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift index 163185ee..fb3188a2 100644 --- a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift @@ -29,6 +29,15 @@ struct HomeFeatureTests { try await verifyHomeTapTodoCategory(adapter: adapter) } + @Test("HomeFeature 카테고리 펼침 버튼은 펼침 상태를 전환한다") + func HomeFeature_카테고리_펼침_버튼은_펼침_상태를_전환한다() async { + let adapter = HomeStoreTestAdapter() + + await adapter.tapTodoCategoryExpansionButton() + + #expect(adapter.isTodoCategoryExpanded) + } + @Test("TodoEditor 생성 delegate는 editor를 닫고 홈 데이터를 다시 조회한다") func TodoEditor_생성_delegate는_editor를_닫고_홈_데이터를_다시_조회한다() async throws { let context = makeHomeFetchDataContext() From f9e656808e773aab7bedb54f3c8cda8febeb5436 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 20 Sep 2026 20:27:24 +0900 Subject: [PATCH 4/5] =?UTF-8?q?ui:=20=ED=99=88=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=EA=B3=BC=20=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeTab/Sources/Home/HomeView.swift | 47 +++++------ .../Sources/Home/TodoCategoryGridLayout.swift | 79 +++++++++++++++++++ 2 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 Application/Presentation/HomeTab/Sources/Home/TodoCategoryGridLayout.swift diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift index 8148a5f8..e99629f7 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift @@ -16,7 +16,7 @@ public struct HomeView: View { @Environment(\.openWindow) private var openWindow @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) - @ScaledMetric(relativeTo: .title2) private var categoryIconSize = CGFloat(48) + @ScaledMetric(relativeTo: .title2) private var categoryIconSize = CGFloat(64) @State private var path = [HomeRoute]() @State private var searchStore: StoreOf @State private var store: StoreOf @@ -124,24 +124,7 @@ public struct HomeView: View { LoadingView() .frame(maxWidth: .infinity) } else { - LazyVGrid( - columns: Array( - repeating: GridItem(.flexible(), spacing: 12), - count: dynamicTypeSize.isAccessibilitySize ? 2 : 4 - ), - spacing: 20 - ) { - let preferences = store.preferences.filter(\.isVisible) - let visiblePreferences = store.isTodoCategoryExpanded - ? preferences - : Array(preferences.prefix(8)) - ForEach(visiblePreferences) { item in - todoCategoryRow(item) - } - if store.isTodoCategoryExpanded { - addTodoCategoryButton - } - } + todoCategoryGrid } HStack { @@ -160,11 +143,31 @@ public struct HomeView: View { Spacer() } } - .padding(.top, 20) .padding(.bottom, 12) .background(Color.surface, in: RoundedRectangle(cornerRadius: 28)) } + @ViewBuilder + private var todoCategoryGrid: some View { + let preferences = store.preferences.filter(\.isVisible) + let visiblePreferences = store.isTodoCategoryExpanded + ? preferences + : Array(preferences.prefix(8)) + + TodoCategoryGridLayout( + columnCount: dynamicTypeSize.isAccessibilitySize ? 2 : 4, + itemWidth: categoryIconSize + ) { + ForEach(visiblePreferences) { item in + todoCategoryRow(item) + } + if store.isTodoCategoryExpanded { + addTodoCategoryButton + } + } + .frame(maxWidth: .infinity) + } + private var addTodoCategoryButton: some View { Button { store.send(.view(.tapManageTodoCategory)) @@ -178,7 +181,7 @@ public struct HomeView: View { .frame(width: categoryIconSize, height: categoryIconSize) .overlay { Image(systemName: "plus") - .font(.title3.bold()) + .font(.title2.bold()) .foregroundStyle(Color.textTertiary) } Text("todo_add", bundle: PresentationResources.bundle) @@ -285,7 +288,7 @@ public struct HomeView: View { .frame(width: categoryIconSize, height: categoryIconSize) .overlay { Image(systemName: item.symbolName) - .font(.title3.bold()) + .font(.title2.bold()) .foregroundStyle(categoryIconForeground(item)) } Text(item.localizedName) diff --git a/Application/Presentation/HomeTab/Sources/Home/TodoCategoryGridLayout.swift b/Application/Presentation/HomeTab/Sources/Home/TodoCategoryGridLayout.swift new file mode 100644 index 00000000..75a3d860 --- /dev/null +++ b/Application/Presentation/HomeTab/Sources/Home/TodoCategoryGridLayout.swift @@ -0,0 +1,79 @@ +// +// TodoCategoryGridLayout.swift +// HomeTab +// +// Created by opfic on 9/20/26. +// + +import SwiftUI + +struct TodoCategoryGridLayout: Layout { + let columnCount: Int + let itemWidth: CGFloat + + func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) -> CGSize { + let width = proposal.width ?? itemWidth * CGFloat(columnCount) + let spacing = spacing(for: width) + let rowHeights = rowHeights(subviews: subviews) + let rowSpacing = spacing * CGFloat(max(0, rowHeights.count - 1)) + + return CGSize( + width: width, + height: spacing + rowHeights.reduce(0, +) + rowSpacing + ) + } + + func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) { + let spacing = spacing(for: bounds.width) + let rowHeights = rowHeights(subviews: subviews) + var currentY = bounds.minY + spacing + + for rowIndex in rowHeights.indices { + let rowStartIndex = rowIndex * columnCount + let rowEndIndex = min(rowStartIndex + columnCount, subviews.count) + + for itemIndex in rowStartIndex.. CGFloat { + max(0, (width - itemWidth * CGFloat(columnCount)) / CGFloat(columnCount + 1)) + } + + private func rowHeights(subviews: Subviews) -> [CGFloat] { + stride(from: 0, to: subviews.count, by: columnCount).map { rowStartIndex in + let rowEndIndex = min(rowStartIndex + columnCount, subviews.count) + + return (rowStartIndex.. Date: Sun, 20 Sep 2026 20:43:57 +0900 Subject: [PATCH 5/5] =?UTF-8?q?ui:=20=ED=99=88=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=88=98=EC=A0=95=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?=EA=B3=BC=20=ED=8E=BC=EC=B9=A8=20=EC=A1=B0=EA=B1=B4=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeTab/Sources/Home/HomeView.swift | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift index e99629f7..cef29b41 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift @@ -83,6 +83,7 @@ public struct HomeView: View { ) } + @ViewBuilder private var topBar: some View { HStack(spacing: 12) { Text("DevLog") @@ -118,7 +119,14 @@ public struct HomeView: View { .background(Color.appBackground) } + @ViewBuilder private var todoSection: some View { + let visibleCategoryCount = store.preferences.filter(\.isVisible).count + let collapsedCategoryCount = min(visibleCategoryCount, 8) + let columnCount = dynamicTypeSize.isAccessibilitySize ? 2 : 4 + let showsExpansionButton = 8 < visibleCategoryCount + || 0 < collapsedCategoryCount && collapsedCategoryCount.isMultiple(of: columnCount) + VStack(alignment: .leading, spacing: 0) { if store.isPreferencesLoading { LoadingView() @@ -127,20 +135,22 @@ public struct HomeView: View { todoCategoryGrid } - HStack { - Spacer() - Button { - store.send( - .view(.tapTodoCategoryExpansionButton), - animation: .easeInOut(duration: 0.2) - ) - } label: { - Image(systemName: store.isTodoCategoryExpanded ? "chevron.up" : "chevron.down") - .font(.title3.weight(.semibold)) - .foregroundStyle(Color.textTertiary) - .frame(width: 32, height: 32) + if !store.isPreferencesLoading, showsExpansionButton { + HStack { + Spacer() + Button { + store.send( + .view(.tapTodoCategoryExpansionButton), + animation: .easeInOut(duration: 0.2) + ) + } label: { + Image(systemName: store.isTodoCategoryExpanded ? "chevron.up" : "chevron.down") + .font(.title3.weight(.semibold)) + .foregroundStyle(Color.textTertiary) + .frame(width: 32, height: 32) + } + Spacer() } - Spacer() } } .padding(.bottom, 12) @@ -153,22 +163,26 @@ public struct HomeView: View { let visiblePreferences = store.isTodoCategoryExpanded ? preferences : Array(preferences.prefix(8)) + let columnCount = dynamicTypeSize.isAccessibilitySize ? 2 : 4 + let hasAvailableSlot = visiblePreferences.isEmpty + || !visiblePreferences.count.isMultiple(of: columnCount) TodoCategoryGridLayout( - columnCount: dynamicTypeSize.isAccessibilitySize ? 2 : 4, + columnCount: columnCount, itemWidth: categoryIconSize ) { ForEach(visiblePreferences) { item in todoCategoryRow(item) } - if store.isTodoCategoryExpanded { - addTodoCategoryButton + if store.isTodoCategoryExpanded || hasAvailableSlot { + editTodoCategoryButton } } .frame(maxWidth: .infinity) } - private var addTodoCategoryButton: some View { + @ViewBuilder + private var editTodoCategoryButton: some View { Button { store.send(.view(.tapManageTodoCategory)) } label: { @@ -180,11 +194,11 @@ public struct HomeView: View { ) .frame(width: categoryIconSize, height: categoryIconSize) .overlay { - Image(systemName: "plus") + Image(systemName: "square.grid.2x2") .font(.title2.bold()) .foregroundStyle(Color.textTertiary) } - Text("todo_add", bundle: PresentationResources.bundle) + Text("todo_edit", bundle: PresentationResources.bundle) .font(.subheadline) .foregroundStyle(Color.textTertiary) }