Skip to content

Commit 5e4b22a

Browse files
authored
[#193] iOS 17에서 PushNotificationListView의 내비게이션바가 inline이 안되는 현상을 해결한다 (#196)
* ui: 볼드체 제거 * fix: iOS 17에서 navigationbar가 투명으로 나타나는 현상 해결 * style: 불필요 뷰 모디파이어 제거 * style: 코드 정리 * fix: 스크롤 트래킹이 안되는 현상 해결 * fix: iOS 17에서는 스크롤뷰를 제거하여 해결
1 parent 6a479f5 commit 5e4b22a

5 files changed

Lines changed: 159 additions & 116 deletions

File tree

DevLog/UI/Common/Component/WebItemRow.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ struct WebItemRow: View {
2626
VStack(alignment: .leading) {
2727
Text(item.title)
2828
.foregroundStyle(Color.primary)
29-
.bold()
3029
.multilineTextAlignment(.leading)
3130
.lineLimit(2)
3231
Text(item.displayURL)

DevLog/UI/Common/NavigationBarConfigurator.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,37 @@
77

88
import SwiftUI
99

10+
/// NavigationBar의 배경색을 지정하고 shadowColor를 제거하는 구조체
11+
///
12+
/// 기본적으로 ``UIColor/systemBackground``를 배경색으로 사용하며,
13+
/// 자체 `NavigationStack`을 가진 뷰에서는 `alwaysVisible`을 `true`로 설정하여
14+
/// 스크롤 위치와 관계없이 배경색이 항상 표시되도록 할 수 있다.
1015
struct NavigationBarConfigurator: UIViewControllerRepresentable {
1116
private let backgroundColor: UIColor
17+
private let alwaysVisible: Bool
1218

19+
/// 지정된 배경색으로 Configurator를 생성한다.
20+
///
21+
/// - Parameter backgroundColor: NavigationBar에 적용할 배경색.
1322
init(_ backgroundColor: UIColor = .systemBackground) {
1423
self.backgroundColor = backgroundColor
24+
self.alwaysVisible = false
25+
}
26+
27+
/// 지정된 배경색과 상시 표시 옵션으로 Configurator를 생성한다.
28+
///
29+
/// - Parameters:
30+
/// - backgroundColor: NavigationBar에 적용할 배경색.
31+
/// - alwaysVisible: `true`이면 스크롤 위치와 관계없이 배경색이 항상 표시된다.
32+
/// 자체 `NavigationStack`을 가진 뷰에서 사용한다.
33+
@available(iOS, deprecated: 18, message: "iOS 18 이상에서는 alwaysVisible 파라미터가 없는 생성자를 사용한다.")
34+
init(_ backgroundColor: UIColor = .systemBackground, alwaysVisible: Bool) {
35+
self.backgroundColor = backgroundColor
36+
if #available(iOS 18.0, *) {
37+
self.alwaysVisible = false
38+
} else {
39+
self.alwaysVisible = alwaysVisible
40+
}
1541
}
1642

1743
func makeCoordinator() -> Coordinator {
@@ -31,6 +57,11 @@ struct NavigationBarConfigurator: UIViewControllerRepresentable {
3157
coordinator.originalShadowColor = navigationBar.standardAppearance.shadowColor
3258
coordinator.originalBackgroundColor = navigationBar.standardAppearance.backgroundColor
3359
}
60+
if self.alwaysVisible, navigationBar.scrollEdgeAppearance == nil {
61+
let appearance = UINavigationBarAppearance()
62+
appearance.configureWithDefaultBackground()
63+
navigationBar.scrollEdgeAppearance = appearance
64+
}
3465
Self.applyAppearance(
3566
to: navigationBar,
3667
shadowColor: .clear,

DevLog/UI/Home/HomeView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ struct HomeView: View {
215215
}
216216
VStack(alignment: .leading) {
217217
Text(todo.title)
218-
.bold()
219218
.foregroundStyle(Color.primary)
220219
Text(todo.dueDate?
221220
.formatted(date: .abbreviated, time: .omitted) ?? "마감일 없음"
@@ -265,7 +264,6 @@ struct HomeView: View {
265264
.foregroundStyle(Color.primary)
266265
.font(.title2.bold())
267266
Spacer()
268-
269267
}
270268
.listRowInsets(EdgeInsets())
271269
}

DevLog/UI/Home/TodoListView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ struct TodoListView: View {
7777
Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left")
7878
}
7979
.navigationTitle(viewModel.state.kind.localizedName)
80-
.navigationBarTitleDisplayMode(.large)
8180
.fullScreenCover(isPresented: Binding(
8281
get: { viewModel.state.showEditor },
8382
set: { viewModel.send(.setShowEditor($0)) }

DevLog/UI/PushNotification/PushNotificationListView.swift

Lines changed: 128 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -18,73 +18,23 @@ struct PushNotificationListView: View {
1818

1919
var body: some View {
2020
NavigationStack(path: $router.path) {
21-
List {
22-
Group {
23-
if viewModel.state.notifications.isEmpty {
24-
HStack {
25-
Spacer()
26-
Text("받은 알림이 없습니다.")
27-
.foregroundStyle(Color.gray)
28-
Spacer()
29-
}
30-
.listRowSeparator(.hidden)
31-
} else {
32-
let notifications = viewModel.state.notifications
33-
ForEach(Array(zip(notifications.indices, notifications)), id: \.1.id) { idx, notification in
34-
Button {
35-
viewModel.send(.tapNotification(notification))
36-
} label: {
37-
notificationRow(notification)
38-
.padding(.vertical, 8)
39-
}
40-
.buttonStyle(.plain)
41-
.onAppear {
42-
let lastID = viewModel.state.notifications.last?.id
43-
if notification.id == lastID, viewModel.state.hasMore {
44-
viewModel.send(.loadNextPage)
45-
}
46-
}
47-
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
48-
.overlay(alignment: .top) {
49-
if #available(iOS 26.0, *) {
50-
if idx == 0 {
51-
Divider()
52-
.padding(.horizontal, -16)
53-
}
54-
}
55-
}
56-
}
57-
}
58-
}
59-
.listSectionSeparator(.hidden, edges: .top)
60-
.listRowBackground(Color.clear)
61-
}
21+
notificationList
6222
.listStyle(.plain)
63-
.background(NavigationBarConfigurator(.secondarySystemBackground))
23+
.background(NavigationBarConfigurator(.secondarySystemBackground, alwaysVisible: true))
6424
.onScrollOffsetChange { offset in
6525
guard isScrollTrackingEnabled else { return }
6626
headerOffset = max(0, -offset)
6727
}
68-
.safeAreaInset(edge: .top) {
69-
VStack(spacing: 4) {
70-
headerView
71-
.clipped()
72-
if #unavailable(iOS 26) {
73-
Divider()
74-
.padding(.horizontal, -16)
75-
}
76-
}
77-
.background {
78-
if #available(iOS 26.0, *) {
79-
Color.clear
80-
} else {
81-
Color(.secondarySystemBackground)
82-
}
28+
.safeAreaInset(edge: .top) { safeAreaHeader }
29+
.background(Color(.secondarySystemBackground))
30+
.onAppear {
31+
viewModel.send(.fetchNotifications)
32+
headerOffset = 0
33+
isScrollTrackingEnabled = false
34+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
35+
isScrollTrackingEnabled = true
8336
}
84-
.offset(y: headerOffset)
8537
}
86-
.background(Color(.secondarySystemBackground))
87-
.onAppear { viewModel.send(.fetchNotifications) }
8838
.refreshable { viewModel.send(.fetchNotifications) }
8939
.navigationTitle("받은 푸시 알람")
9040
.alert(
@@ -138,77 +88,143 @@ struct PushNotificationListView: View {
13888
}
13989
}
14090

141-
private var headerView: some View {
142-
ScrollView(.horizontal) {
143-
HStack(spacing: 8) {
144-
if 0 < viewModel.appliedFilterCount {
145-
Menu {
146-
Text("\(viewModel.appliedFilterCount)개 필터가 적용됨")
147-
Button(role: .destructive) {
148-
viewModel.send(.resetFilters)
91+
private var notificationList: some View {
92+
List {
93+
Group {
94+
if viewModel.state.notifications.isEmpty {
95+
HStack {
96+
Spacer()
97+
Text("받은 알림이 없습니다.")
98+
.foregroundStyle(Color.gray)
99+
Spacer()
100+
}
101+
.listRowSeparator(.hidden)
102+
} else {
103+
let notifications = viewModel.state.notifications
104+
ForEach(Array(zip(notifications.indices, notifications)), id: \.1.id) { idx, notification in
105+
Button {
106+
viewModel.send(.tapNotification(notification))
149107
} label: {
150-
Text("모든 필터 지우기")
108+
notificationRow(notification)
109+
.padding(.vertical, 8)
151110
}
152-
} label: {
153-
HStack(spacing: 6) {
154-
Image(systemName: "line.3.horizontal.decrease")
155-
filterBadge
111+
.buttonStyle(.plain)
112+
.onAppear {
113+
let lastID = viewModel.state.notifications.last?.id
114+
if notification.id == lastID, viewModel.state.hasMore {
115+
viewModel.send(.loadNextPage)
116+
}
117+
}
118+
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
119+
.overlay(alignment: .top) {
120+
if #available(iOS 26.0, *) {
121+
if idx == 0 {
122+
Divider()
123+
.padding(.horizontal, -16)
124+
}
125+
}
156126
}
157-
.adaptiveButtonStyle()
158127
}
159128
}
129+
}
130+
.listSectionSeparator(.hidden, edges: .top)
131+
.listRowBackground(Color.clear)
132+
}
133+
}
160134

161-
Button {
162-
viewModel.send(.toggleSortOption)
163-
} label: {
164-
let condition = viewModel.state.query.sortOrder == .oldest
165-
Text("정렬: \(viewModel.state.query.sortOrder.title)")
166-
.foregroundStyle(condition ? .white : Color(.label))
167-
.adaptiveButtonStyle(color: condition ? .blue : .clear)
168-
}
135+
private var safeAreaHeader: some View {
136+
VStack(spacing: 4) {
137+
headerView
138+
.clipped()
139+
if #unavailable(iOS 26) {
140+
Divider()
141+
.padding(.horizontal, -16)
142+
}
143+
}
144+
.background {
145+
if #available(iOS 26.0, *) {
146+
Color.clear
147+
} else {
148+
Color(.secondarySystemBackground)
149+
}
150+
}
151+
.offset(y: headerOffset)
152+
}
169153

154+
private var headerView: some View {
155+
Group {
156+
if #available(iOS 18, *) {
157+
ScrollView(.horizontal) { headerContent }
158+
.scrollIndicators(.never)
159+
.scrollDisabled(!isScrollTrackingEnabled)
160+
.contentMargins(.leading, 16, for: .scrollContent)
161+
} else {
162+
headerContent
163+
.padding(.leading, 16)
164+
.frame(maxWidth: .infinity, alignment: .leading)
165+
}
166+
}
167+
}
168+
169+
private var headerContent: some View {
170+
HStack(spacing: 8) {
171+
if 0 < viewModel.appliedFilterCount {
170172
Menu {
171-
Picker(selection: Binding(
172-
get: { viewModel.state.query.timeFilter },
173-
set: { viewModel.send(.setTimeFilter($0)) }
174-
)) {
175-
ForEach(PushNotificationQuery.TimeFilter.availableOptions, id: \.self) { option in
176-
Text(option.title).tag(option)
177-
}
173+
Text("\(viewModel.appliedFilterCount)개 필터가 적용됨")
174+
Button(role: .destructive) {
175+
viewModel.send(.resetFilters)
178176
} label: {
179-
Text("기간")
177+
Text("모든 필터 지우기")
180178
}
181179
} label: {
182-
let condition = viewModel.state.query.timeFilter == .none
183-
HStack {
184-
Text("기간")
185-
Image(systemName: "chevron.down")
180+
HStack(spacing: 6) {
181+
Image(systemName: "line.3.horizontal.decrease")
182+
filterBadge
186183
}
187-
.foregroundStyle(condition ? Color(.label) : .white)
188-
.adaptiveButtonStyle(color: condition ? .clear : .blue)
184+
.adaptiveButtonStyle()
189185
}
186+
}
190187

191-
Button {
192-
viewModel.send(.toggleUnreadOnly)
188+
Button {
189+
viewModel.send(.toggleSortOption)
190+
} label: {
191+
let condition = viewModel.state.query.sortOrder == .oldest
192+
Text("정렬: \(viewModel.state.query.sortOrder.title)")
193+
.foregroundStyle(condition ? .white : Color(.label))
194+
.adaptiveButtonStyle(color: condition ? .blue : .clear)
195+
}
196+
197+
Menu {
198+
Picker(selection: Binding(
199+
get: { viewModel.state.query.timeFilter },
200+
set: { viewModel.send(.setTimeFilter($0)) }
201+
)) {
202+
ForEach(PushNotificationQuery.TimeFilter.availableOptions, id: \.self) { option in
203+
Text(option.title).tag(option)
204+
}
193205
} label: {
194-
let condition = viewModel.state.query.unreadOnly
195-
Text("읽지 않음")
196-
.foregroundStyle(condition ? .white : Color(.label))
197-
.adaptiveButtonStyle(color: condition ? .blue : .clear)
206+
Text("기간")
198207
}
208+
} label: {
209+
let condition = viewModel.state.query.timeFilter == .none
210+
HStack {
211+
Text("기간")
212+
Image(systemName: "chevron.down")
213+
}
214+
.foregroundStyle(condition ? Color(.label) : .white)
215+
.adaptiveButtonStyle(color: condition ? .clear : .blue)
199216
}
200-
.frame(height: 36)
201-
}
202-
.scrollIndicators(.never)
203-
.scrollDisabled(!isScrollTrackingEnabled)
204-
.contentMargins(.leading, 16, for: .scrollContent)
205-
.onAppear {
206-
headerOffset = 0
207-
isScrollTrackingEnabled = false
208-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
209-
isScrollTrackingEnabled = true
217+
218+
Button {
219+
viewModel.send(.toggleUnreadOnly)
220+
} label: {
221+
let condition = viewModel.state.query.unreadOnly
222+
Text("읽지 않음")
223+
.foregroundStyle(condition ? .white : Color(.label))
224+
.adaptiveButtonStyle(color: condition ? .blue : .clear)
210225
}
211226
}
227+
.frame(height: 36)
212228
}
213229

214230
private var filterBadge: some View {

0 commit comments

Comments
 (0)