diff --git a/DevLog/Resource/Assets.xcassets/Apple.imageset/Contents.json b/DevLog/Resource/Assets.xcassets/Apple.imageset/Contents.json index 80ffdaaa..17f02871 100644 --- a/DevLog/Resource/Assets.xcassets/Apple.imageset/Contents.json +++ b/DevLog/Resource/Assets.xcassets/Apple.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "filename" : "Apple_black.png", - "idiom" : "iphone" + "idiom" : "universal" }, { "appearances" : [ @@ -12,7 +12,7 @@ } ], "filename" : "Apple_white.png", - "idiom" : "iphone" + "idiom" : "universal" } ], "info" : { diff --git a/DevLog/Resource/Assets.xcassets/Github.imageset/Contents.json b/DevLog/Resource/Assets.xcassets/Github.imageset/Contents.json index efbdb0d1..3f6384bd 100644 --- a/DevLog/Resource/Assets.xcassets/Github.imageset/Contents.json +++ b/DevLog/Resource/Assets.xcassets/Github.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "filename" : "Github_black.png", - "idiom" : "iphone" + "idiom" : "universal" }, { "appearances" : [ @@ -12,7 +12,7 @@ } ], "filename" : "Github_white.png", - "idiom" : "iphone" + "idiom" : "universal" } ], "info" : { diff --git a/DevLog/Resource/Assets.xcassets/Google.imageset/Contents.json b/DevLog/Resource/Assets.xcassets/Google.imageset/Contents.json index f87048f4..89ac1240 100644 --- a/DevLog/Resource/Assets.xcassets/Google.imageset/Contents.json +++ b/DevLog/Resource/Assets.xcassets/Google.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "filename" : "Google.png", - "idiom" : "iphone" + "idiom" : "universal" } ], "info" : { diff --git a/DevLog/Resource/Assets.xcassets/Primary.imageset/Contents.json b/DevLog/Resource/Assets.xcassets/Primary.imageset/Contents.json index d1e8f444..d35a699e 100644 --- a/DevLog/Resource/Assets.xcassets/Primary.imageset/Contents.json +++ b/DevLog/Resource/Assets.xcassets/Primary.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "filename" : "Primary_dark.png", - "idiom" : "iphone" + "idiom" : "universal" } ], "info" : { diff --git a/DevLog/UI/Common/Component/LoginButton.swift b/DevLog/UI/Common/Component/LoginButton.swift index a0a6f2f3..1f925003 100644 --- a/DevLog/UI/Common/Component/LoginButton.swift +++ b/DevLog/UI/Common/Component/LoginButton.swift @@ -10,8 +10,8 @@ import SwiftUI struct LoginButton: View { @State private var logo: Image? @State private var text = "" - @State private var height = CGFloat.zero - let action: () -> Void + @ScaledMetric(relativeTo: .body) private var height = CGFloat(22) + private let action: () -> Void init( logo: Image? = nil, @@ -27,31 +27,24 @@ struct LoginButton: View { Button(action: { action() }) { - HStack { - Text(text) - .foregroundStyle(Color.primary) - .font(.system(size: height / 3)) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } + Text(text) + .foregroundStyle(Color.primary) + .font(.system(.body)) } - .contentShape(RoundedRectangle(cornerRadius: height / 2)) - .overlay( - GeometryReader { proxy in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: height / 2) - .stroke(Color.gray, lineWidth: 1) - .onAppear { - height = proxy.size.height - } - if let logo = logo { - logo - .resizable() - .scaledToFit() - .frame(width: height / 2, height: height / 2) - .padding(.leading) - } + .frame(width: 300, height: height + 16) + .contentShape(.capsule) + .overlay { + ZStack(alignment: .leading) { + Capsule() + .stroke(Color.gray, lineWidth: 1) + if let logo = logo { + logo + .resizable() + .scaledToFit() + .frame(width: height, height: height) + .padding(.leading) } } - ) + } } } diff --git a/DevLog/UI/Common/Component/TodoItemRow.swift b/DevLog/UI/Common/Component/TodoItemRow.swift index 7a878ca0..370025b5 100644 --- a/DevLog/UI/Common/Component/TodoItemRow.swift +++ b/DevLog/UI/Common/Component/TodoItemRow.swift @@ -8,7 +8,7 @@ import SwiftUI struct TodoItemRow: View { - @Environment(\.sceneWidth) private var sceneWidth + @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) private let item: TodoListItem init(_ item: TodoListItem) { @@ -19,7 +19,7 @@ struct TodoItemRow: View { HStack { Image(systemName: "checkmark.circle") .resizable() - .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) + .frame(width: labelWidth, height: labelWidth) .foregroundStyle(item.isCompleted ? .green : .secondary) VStack(alignment: .leading, spacing: 0) { HStack { diff --git a/DevLog/UI/Common/Component/WebItemRow.swift b/DevLog/UI/Common/Component/WebItemRow.swift index d05d4523..e1119172 100644 --- a/DevLog/UI/Common/Component/WebItemRow.swift +++ b/DevLog/UI/Common/Component/WebItemRow.swift @@ -8,15 +8,14 @@ import SwiftUI struct WebItemRow: View { - @Environment(\.sceneWidth) private var sceneWidth - + @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) let item: WebPageItem let showsChevron: Bool var body: some View { HStack { thumbnail - .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) + .frame(width: labelWidth, height: labelWidth) .clipShape(RoundedRectangle(cornerRadius: 10)) VStack(alignment: .leading) { diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index 9a704266..64a6e655 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -9,9 +9,9 @@ import SwiftUI struct HomeView: View { @Environment(\.diContainer) var container: any DIContainer - @Environment(\.sceneWidth) var sceneWidth: CGFloat @State private var router = NavigationRouter() @State var viewModel: HomeViewModel + @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) var body: some View { NavigationStack(path: $router.path) { @@ -210,7 +210,7 @@ struct HomeView: View { } else { ForEach(viewModel.state.recentTodos, id: \.id) { todo in NavigationLink(value: Path.detail(todo.id)) { - RecentTodoRow(todo: todo, sceneWidth: sceneWidth) + RecentTodoRow(todo: todo) } } } @@ -370,7 +370,7 @@ struct HomeView: View { HStack { RoundedRectangle(cornerRadius: 8) .fill(imageColor) - .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) + .frame(width: labelWidth, height: labelWidth) .overlay { Image(systemName: systemName) .foregroundStyle(Color.white) @@ -391,15 +391,15 @@ struct HomeView: View { } private struct RecentTodoRow: View { + @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) let todo: RecentTodoItem - let sceneWidth: CGFloat var body: some View { let category = TodoCategoryItem(from: todo.category) HStack(alignment: .top, spacing: 12) { RoundedRectangle(cornerRadius: 8) .fill(category.color) - .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) + .frame(width: labelWidth, height: labelWidth) .overlay { Image(systemName: category.symbolName) .foregroundStyle(Color.white) diff --git a/DevLog/UI/Login/LoginView.swift b/DevLog/UI/Login/LoginView.swift index 30fafdfd..52c89a4b 100644 --- a/DevLog/UI/Login/LoginView.swift +++ b/DevLog/UI/Login/LoginView.swift @@ -25,17 +25,14 @@ struct LoginView: View { LoginButton(logo: Image("Google"), text: String(localized: "login_google_sign_in")) { viewModel.send(.tapSignInButton(.google)) } - .frame(width: sceneWidth * 3 / 4, height: sceneWidth / 10) LoginButton(logo: Image("Github"), text: String(localized: "login_github_sign_in")) { viewModel.send(.tapSignInButton(.github)) } - .frame(width: sceneWidth * 3 / 4, height: sceneWidth / 10) LoginButton(logo: Image("Apple"), text: String(localized: "login_apple_sign_in")) { viewModel.send(.tapSignInButton(.apple)) } - .frame(width: sceneWidth * 3 / 4, height: sceneWidth / 10) } .padding(.bottom, 30) Text(String(localized: "login_terms_notice")) diff --git a/DevLog/UI/Profile/HeatmapView.swift b/DevLog/UI/Profile/HeatmapView.swift index 617d11d5..c8861c88 100644 --- a/DevLog/UI/Profile/HeatmapView.swift +++ b/DevLog/UI/Profile/HeatmapView.swift @@ -8,8 +8,7 @@ import SwiftUI struct HeatmapView: View { - @Environment(\.safeAreaInsets) private var safeAreaInsets - @Environment(\.sceneWidth) private var sceneWidth + @State private var availableWidth = CGFloat.zero let quarter: HeatmapQuarter let selectedActivityKinds: Set let selectedDay: HeatmapDay? @@ -21,31 +20,32 @@ struct HeatmapView: View { weekCounts: quarter.months.map(\.weeks.count) ) - HStack(alignment: .top, spacing: layout.monthSpacing) { - ForEach(quarter.months) { month in - MonthCompactHeatmapView( - month: month, - maxCount: maxCount, - layout: layout, - selectedActivityKinds: selectedActivityKinds, - selectedDay: selectedDay, - onSelectDay: onSelectDay - ) + ScrollView(.horizontal) { + LazyHStack(alignment: .top, spacing: layout.monthSpacing) { + ForEach(quarter.months) { month in + MonthCompactHeatmapView( + month: month, + maxCount: maxCount, + layout: layout, + selectedActivityKinds: selectedActivityKinds, + selectedDay: selectedDay, + onSelectDay: onSelectDay + ) + } + } + .padding(.vertical, 2) + .frame(width: layout.contentWidth, alignment: .leading) + } + .scrollIndicators(.hidden) + .scrollDisabled(true) + .frame(maxWidth: .infinity, alignment: .leading) + .background { + GeometryReader { geometry in + Color.clear + .onAppear { updateAvailableWidth(geometry.size.width) } + .onChange(of: geometry.size.width) { updateAvailableWidth($1) } } } - .padding(.vertical, 2) - } - - private var availableWidth: CGFloat { - // ProfileView의 바깥 가로 패딩(16)과 히트맵 카드 내부 패딩(12)을 합한 값 - let horizontalPadding: CGFloat = 16 + 12 - return max( - 0, - sceneWidth - - safeAreaInsets.leading - - safeAreaInsets.trailing - - (horizontalPadding * 2) - ) } private var maxCount: Int { @@ -70,13 +70,22 @@ struct HeatmapView: View { } return value } + + private func updateAvailableWidth(_ width: CGFloat) { + if availableWidth != width { + availableWidth = width + } + } } private struct HeatmapLayout { + private static let minimumCellSize: CGFloat = 8 + private static let maximumCellSize: CGFloat = 22 let cellSize: CGFloat let cellSpacing: CGFloat = 4 let monthSpacing: CGFloat = 12 let monthTitleSpacing: CGFloat = 6 + let contentWidth: CGFloat init(availableWidth: CGFloat, weekCounts: [Int]) { let totalColumns = max(weekCounts.reduce(0, +), 1) @@ -85,7 +94,9 @@ private struct HeatmapLayout { } let fixedWidth = monthSpacing * CGFloat(max(weekCounts.count - 1, 0)) + cellSpacing * CGFloat(totalColumnSpacings) - cellSize = max(0, availableWidth - fixedWidth) / CGFloat(totalColumns) + let fittingCellSize = max(0, availableWidth - fixedWidth) / CGFloat(totalColumns) + cellSize = min(max(fittingCellSize, Self.minimumCellSize), Self.maximumCellSize) + contentWidth = cellSize * CGFloat(totalColumns) + fixedWidth } var cellCornerRadius: CGFloat { diff --git a/DevLog/UI/PushNotification/PushNotificationListView.swift b/DevLog/UI/PushNotification/PushNotificationListView.swift index 593b463e..7b5b433a 100644 --- a/DevLog/UI/PushNotification/PushNotificationListView.swift +++ b/DevLog/UI/PushNotification/PushNotificationListView.swift @@ -10,12 +10,12 @@ import SwiftUI struct PushNotificationListView: View { @State private var router = NavigationRouter() @State var viewModel: PushNotificationListViewModel - @Environment(\.sceneWidth) private var sceneWidth @Environment(\.colorScheme) private var colorScheme @Environment(\.diContainer) private var container: DIContainer @ScaledMetric(relativeTo: .body) private var headerHeight = 41 @State private var headerOffset: CGFloat = 0 @State private var isScrollTrackingEnabled = false + @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) var body: some View { NavigationStack(path: $router.path) { @@ -266,7 +266,7 @@ struct PushNotificationListView: View { let todoCategoryItem = TodoCategoryItem(from: item.todoCategory) RoundedRectangle(cornerRadius: 8) .fill(todoCategoryItem.color) - .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) + .frame(width: labelWidth, height: labelWidth) .overlay { Image(systemName: todoCategoryItem.symbolName) .foregroundStyle(Color.white) diff --git a/DevLog/UI/Search/SearchView.swift b/DevLog/UI/Search/SearchView.swift index 2c63c3c5..8db33875 100644 --- a/DevLog/UI/Search/SearchView.swift +++ b/DevLog/UI/Search/SearchView.swift @@ -9,7 +9,6 @@ import SwiftUI struct SearchView: View { @Environment(\.dismiss) private var dismiss - @Environment(\.sceneWidth) private var sceneWidth @Environment(\.diContainer) private var container: DIContainer @State private var router = NavigationRouter() @State var viewModel: SearchViewModel