From 785bb0131f41dfca4b2e32bc2134b0ef80d285d1 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 13 Sep 2026 13:18:31 +0900 Subject: [PATCH 1/5] =?UTF-8?q?ui:=20.sidebarAdaptable=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Presentation/Entry/Sources/Main/MainView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index 8fda5635..37e6d183 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -52,7 +52,6 @@ struct MainView: View { tabLabel(.profile) } } - .tabViewStyle(.sidebarAdaptable) .toastHost() .onAppear { store.send(.view(.onAppear)) } .onChange(of: selectedTab, initial: true) { _, tab in From 3a745049e1cfdd1651573c4eb4d68b54ab0f6e27 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 13 Sep 2026 14:14:32 +0900 Subject: [PATCH 2/5] =?UTF-8?q?ui:=20=EB=A9=94=EC=9D=B8=20=ED=83=90?= =?UTF-8?q?=EC=83=89=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entry/Sources/Main/MainView.swift | 192 +++++++++++++++--- 1 file changed, 168 insertions(+), 24 deletions(-) diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index 37e6d183..2a04cf77 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -13,6 +13,8 @@ import PresentationShared import TodayTab struct MainView: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @Binding var selectedTab: MainTab @State private var store: StoreOf private let windowEvent: TodoEditorWindowEvent @@ -29,27 +31,49 @@ struct MainView: View { } var body: some View { - TabView(selection: $selectedTab) { - Tab(value: MainTab.home) { - tabContent(.home) - } label: { - tabLabel(.home) + HStack(spacing: 0) { + if showsSideBar { + SideBar( + selectedTab: $selectedTab, + unreadPushCount: store.unreadPushCount + ) + .resizableSideBar( + minimumWidth: 220, + idealWidth: 260, + maximumWidth: 320 + ) } - Tab(value: MainTab.today) { - tabContent(.today) - } label: { - tabLabel(.today) - } - Tab(value: MainTab.notification) { - tabContent(.notification) - } label: { - tabLabel(.notification) - } - .badge(store.unreadPushCount) - Tab(value: MainTab.profile) { - tabContent(.profile) - } label: { - tabLabel(.profile) + + TabView(selection: $selectedTab) { + Tab( + MainTab.home.title, + systemImage: MainTab.home.symbolName, + value: MainTab.home + ) { + tabContent(.home) + } + Tab( + MainTab.today.title, + systemImage: MainTab.today.symbolName, + value: MainTab.today + ) { + tabContent(.today) + } + Tab( + MainTab.notification.title, + systemImage: MainTab.notification.symbolName, + value: MainTab.notification + ) { + tabContent(.notification) + } + .badge(store.unreadPushCount) + Tab( + MainTab.profile.title, + systemImage: MainTab.profile.symbolName, + value: MainTab.profile + ) { + tabContent(.profile) + } } } .toastHost() @@ -60,15 +84,16 @@ struct MainView: View { .prominentAlert(store, state: \.alert, action: \.alert) } + private var showsSideBar: Bool { + isiOSAppOnMac || horizontalSizeClass == .regular + } + @ViewBuilder private func tabContent(_ tab: MainTab) -> some View { let isSelected = selectedTab == tab tabView(tab, isSelected: isSelected) .environment(\.isTabContentActive, isSelected) - } - - private func tabLabel(_ tab: MainTab) -> some View { - Label(tab.title, systemImage: tab.symbolName) + .toolbarVisibility(showsSideBar ? .hidden : .visible, for: .tabBar) } @ViewBuilder @@ -94,3 +119,122 @@ struct MainView: View { } } } + +private struct SideBar: View { + @Binding var selectedTab: MainTab + let unreadPushCount: Int + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + ForEach(MainTab.allCases, id: \.self) { tab in + tabButton(tab) + } + Spacer(minLength: 0) + } + .padding(.horizontal, 12) + .padding(.vertical, 16) + .background(Color.surface) + } + + private func tabButton(_ tab: MainTab) -> some View { + let isSelected = selectedTab == tab + + return Button { + selectedTab = tab + } label: { + HStack(spacing: 12) { + Image(systemName: tab.symbolName) + .font(.title3) + .frame(width: 24) + Text(tab.title) + .font(.body.weight(.semibold)) + .lineLimit(1) + Spacer(minLength: 8) + if tab == .notification, 0 < unreadPushCount { + Text(verbatim: unreadPushCount.formatted()) + .font(.caption.bold()) + .monospacedDigit() + .foregroundStyle(Color.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.accent, in: .capsule) + } + } + .foregroundStyle(isSelected ? Color.onPrimaryContainer : .primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background { + RoundedRectangle(cornerRadius: 12) + .fill(isSelected ? Color.primaryContainer : .clear) + } + .contentShape(.rect) + } + .buttonStyle(.plain) + } +} + +private struct ResizableSideBarModifier: ViewModifier { + @Environment(\.layoutDirection) private var layoutDirection + @State private var width: CGFloat + @State private var dragStartWidth: CGFloat? + private let minimumWidth: CGFloat + private let maximumWidth: CGFloat + + init( + minimumWidth: CGFloat, + idealWidth: CGFloat, + maximumWidth: CGFloat + ) { + self.minimumWidth = minimumWidth + self.maximumWidth = maximumWidth + self._width = State(initialValue: min(max(idealWidth, minimumWidth), maximumWidth)) + } + + func body(content: Content) -> some View { + content + .frame(width: width) + .overlay(alignment: .trailing) { + Color.clear + .frame(width: 16) + .contentShape(.rect) + .gesture(resizeGesture) + .overlay(alignment: .trailing) { + Rectangle() + .fill(Color.border) + .frame(width: 1) + } + } + } + + private var resizeGesture: some Gesture { + DragGesture(minimumDistance: 0, coordinateSpace: .global) + .onChanged { value in + if dragStartWidth == nil { + dragStartWidth = width + } + let startWidth = dragStartWidth ?? width + let translation = layoutDirection == .leftToRight + ? value.translation.width + : -value.translation.width + width = min(max(startWidth + translation, minimumWidth), maximumWidth) + } + .onEnded { _ in + dragStartWidth = nil + } + } +} + +private extension View { + func resizableSideBar( + minimumWidth: CGFloat, + idealWidth: CGFloat, + maximumWidth: CGFloat + ) -> some View { + modifier(ResizableSideBarModifier( + minimumWidth: minimumWidth, + idealWidth: idealWidth, + maximumWidth: maximumWidth + )) + } +} From a10c4ecb7ce2cbc73101d0403f3f4ab06192c531 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 13 Sep 2026 14:38:21 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94?= =?UTF-8?q?=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=EC=95=88=EC=A0=84=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Presentation/Entry/Sources/Main/MainView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index 2a04cf77..66f925a1 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -133,7 +133,10 @@ private struct SideBar: View { } .padding(.horizontal, 12) .padding(.vertical, 16) - .background(Color.surface) + .background { + Color.surface + .ignoresSafeArea(.container, edges: .vertical) + } } private func tabButton(_ tab: MainTab) -> some View { @@ -204,6 +207,7 @@ private struct ResizableSideBarModifier: ViewModifier { .fill(Color.border) .frame(width: 1) } + .ignoresSafeArea(.container, edges: .vertical) } } From 1daf7d7f8e9b5085722312f43468be8d91307940 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 13 Sep 2026 14:45:25 +0900 Subject: [PATCH 4/5] =?UTF-8?q?ui:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=ED=83=AD=EC=9D=80=20=EB=A7=A8=20=EC=95=84=EB=9E=98=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Presentation/Entry/Sources/Main/MainView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index 66f925a1..b818e12d 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -126,10 +126,11 @@ private struct SideBar: View { var body: some View { VStack(alignment: .leading, spacing: 8) { - ForEach(MainTab.allCases, id: \.self) { tab in + let tabs = MainTab.allCases + ForEach(Array(zip(tabs.indices, tabs)), id: \.1) { index, tab in + if index == tabs.count - 1 { Spacer() } tabButton(tab) } - Spacer(minLength: 0) } .padding(.horizontal, 12) .padding(.vertical, 16) From f1490a7720fb228833d8b9f072d963f3b5f9b06d Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 13 Sep 2026 16:13:30 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20iPad=20=EC=B0=BD=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=20=EC=98=81=EC=97=AD=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=97=AC=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entry/Sources/Main/MainView.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index b818e12d..721dab02 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -125,21 +125,29 @@ private struct SideBar: View { let unreadPushCount: Int var body: some View { - VStack(alignment: .leading, spacing: 8) { - let tabs = MainTab.allCases - ForEach(Array(zip(tabs.indices, tabs)), id: \.1) { index, tab in - if index == tabs.count - 1 { Spacer() } - tabButton(tab) + GeometryReader { geometry in + VStack(alignment: .leading, spacing: 8) { + ForEach(MainTab.allCases, id: \.self) { tab in + if tab == .profile { Spacer() } + tabButton(tab) + } } + .padding(.horizontal, 12) + .padding(.vertical, 16) + .padding(.top, windowControlTopPadding(in: geometry)) } - .padding(.horizontal, 12) - .padding(.vertical, 16) .background { Color.surface .ignoresSafeArea(.container, edges: .vertical) } } + private func windowControlTopPadding(in geometry: GeometryProxy) -> CGFloat { + guard #available(iOS 26.0, *), + UIDevice.current.userInterfaceIdiom == .pad else { return 0 } + return geometry.containerCornerInsets.topLeading.height + } + private func tabButton(_ tab: MainTab) -> some View { let isSelected = selectedTab == tab