diff --git a/.swiftformat b/.swiftformat index 21f72f9..b12b883 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,2 +1,3 @@ ---swiftversion 5.8 ---disable preferKeyPath \ No newline at end of file +--swiftversion 6.0 +--disable preferKeyPath +--ifdef no-indent \ No newline at end of file diff --git a/AppExample/Example/Router/Alerts.swift b/AppExample/Example/Router/Alerts.swift index 4b1c578..d50317c 100644 --- a/AppExample/Example/Router/Alerts.swift +++ b/AppExample/Example/Router/Alerts.swift @@ -16,30 +16,30 @@ enum RootAlert: Identifiable { var id: String { switch self { case .dismiss: - return "dismiss" + "dismiss" case .delete: - return "delete" + "delete" case .appError: - return "appError" + "appError" } } var alert: Alert { switch self { case let .dismiss(action): - return Alert( + Alert( title: Text("Are you sure you want to dismiss?"), primaryButton: .destructive(Text("Dismiss"), action: action), secondaryButton: .cancel() ) case let .delete(action): - return Alert( + Alert( title: Text("Are you sure you want to delete?"), primaryButton: .destructive(Text("\(L10n.Button.delete)"), action: action), secondaryButton: .cancel() ) case let .appError(error: error): - return Alert( + Alert( title: Text(error.title), message: Text(error.subtitle.valueOrEmpty), dismissButton: .cancel() diff --git a/AppExample/Example/Router/Router.swift b/AppExample/Example/Router/Router.swift index 3bbf5fd..7c5062f 100644 --- a/AppExample/Example/Router/Router.swift +++ b/AppExample/Example/Router/Router.swift @@ -201,9 +201,9 @@ extension Router { extension Screen: Hashable, Equatable { static func == (lhs: Screen, rhs: Screen) -> Bool { if lhs.id == rhs.id { - return true + true } else { - return false + false } } diff --git a/AppExample/Example/Router/Screens.swift b/AppExample/Example/Router/Screens.swift index 0335d8d..5999126 100644 --- a/AppExample/Example/Router/Screens.swift +++ b/AppExample/Example/Router/Screens.swift @@ -16,9 +16,9 @@ extension Screen: Identifiable { var id: String { switch self { case .settings: - return "settings" + "settings" case .premium: - return "premium" + "premium" } } } diff --git a/AppExample/Example/Router/Tabs.swift b/AppExample/Example/Router/Tabs.swift index 5ecbe73..e1ccd9e 100644 --- a/AppExample/Example/Router/Tabs.swift +++ b/AppExample/Example/Router/Tabs.swift @@ -15,45 +15,45 @@ public enum RootTab: String { var id: String { switch self { case .main: - return "home" + "home" case .secondary: - return "secondary" + "secondary" case .tertiary: - return "tertiary" + "tertiary" case .quaternary: - return "quaternary" + "quaternary" case .settings: - return "settings" + "settings" } } var title: String { switch self { case .main: - return "Home" + "Home" case .secondary: - return "Secondary" + "Secondary" case .tertiary: - return "Tertiary" + "Tertiary" case .quaternary: - return "Quaternary" + "Quaternary" case .settings: - return "Settings" + "Settings" } } var image: Image { switch self { case .main: - return Image(systemName: "") + Image(systemName: "") case .secondary: - return Image(systemName: "") + Image(systemName: "") case .tertiary: - return Image(systemName: "") + Image(systemName: "") case .quaternary: - return Image(systemName: "") + Image(systemName: "") case .settings: - return Image(systemName: "") + Image(systemName: "") } } } diff --git a/Package.swift b/Package.swift index 1795262..c9f467a 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let developmentDependencies: [PackageDescription.Package.Dependency] = [ .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), ] -var dependencies: [PackageDescription.Package.Dependency] = remoteDependencies +var dependencies: [PackageDescription.Package.Dependency] = developmentDependencies if ProcessInfo.processInfo.environment["BUILD_MODE"] == "DEV" { dependencies = developmentDependencies diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift index cfdb2ca..df0afe4 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventView.swift @@ -4,7 +4,7 @@ // #if canImport(EventKit) - import EventKit +import EventKit #endif import MapKit import OversizeCalendarService @@ -16,316 +16,270 @@ import OversizeUI import SwiftUI #if !os(tvOS) - public struct CreateEventView: View { - @StateObject var viewModel: CreateEventViewModel - @Environment(\.dismiss) var dismiss - @FocusState private var focusedField: FocusField? +public struct CreateEventView: View { + @StateObject var viewModel: CreateEventViewModel + @Environment(\.dismiss) var dismiss + @FocusState private var focusedField: FocusField? - public init(_ type: CreateEventType = .new(nil, calendar: nil)) { - _viewModel = StateObject(wrappedValue: CreateEventViewModel(type)) - } + public init(_ type: CreateEventType = .new(nil, calendar: nil)) { + _viewModel = StateObject(wrappedValue: CreateEventViewModel(type)) + } - public var body: some View { - PageView { - content() - } - .leadingBar { - BarButton(.closeAction { - dismiss() - }) - } - .trailingBar { - BarButton(.accent(L10n.Button.save, action: { - switch viewModel.type { - case .new: + public var body: some View { + PageView { + content() + } + .leadingBar { + BarButton(.closeAction { + dismiss() + }) + } + .trailingBar { + BarButton(.accent(L10n.Button.save, action: { + switch viewModel.type { + case .new: + Task { + _ = await viewModel.save() + dismiss() + } + case .update: + if viewModel.span == nil, viewModel.repitRule != .never { + viewModel.present(.span) + } else { Task { _ = await viewModel.save() dismiss() } - case .update: - if viewModel.span == nil, viewModel.repitRule != .never { - viewModel.present(.span) - } else { - Task { - _ = await viewModel.save() - dismiss() - } - } - } - })) - .disabled(viewModel.title.isEmpty) - } - .titleLabel { - Button { viewModel.present(.calendar) } label: { - HStack(spacing: .xxxSmall) { - Circle() - .fill(Color(viewModel.calendar?.cgColor ?? CGColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1))) - .frame(width: 16, height: 16) - .padding(.xxxSmall) - - Text(viewModel.calendar?.title ?? "") - .padding(.trailing, .xxSmall) } } - .buttonStyle(.tertiary) - .controlBorderShape(.capsule) - .controlSize(.mini) - } - .navigationBarDividerColor(Color.onSurfacePrimary.opacity(0.1)) - .safeAreaInset(edge: .bottom) { - bottomBar - } - .task { - await viewModel.fetchData() - } - .onAppear { - focusedField = .title - } - .sheet(item: $viewModel.sheet) { sheet in - resolveSheet(sheet: sheet) - } - .onChange(of: viewModel.span) { _ in - Task { - _ = await viewModel.save() - dismiss() + })) + .disabled(viewModel.title.isEmpty) + } + .titleLabel { + Button { viewModel.present(.calendar) } label: { + HStack(spacing: .xxxSmall) { + Circle() + .fill(Color(viewModel.calendar?.cgColor ?? CGColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1))) + .frame(width: 16, height: 16) + .padding(.xxxSmall) + + Text(viewModel.calendar?.title ?? "") + .padding(.trailing, .xxSmall) } } + .buttonStyle(.tertiary) + .controlBorderShape(.capsule) + .controlSize(.mini) } + .navigationBarDividerColor(Color.onSurfacePrimary.opacity(0.1)) + .safeAreaInset(edge: .bottom) { + bottomBar + } + .task { + await viewModel.fetchData() + } + .onAppear { + focusedField = .title + } + .sheet(item: $viewModel.sheet) { sheet in + resolveSheet(sheet: sheet) + } + .onChange(of: viewModel.span) { _ in + Task { + _ = await viewModel.save() + dismiss() + } + } + } - @ViewBuilder - private func content() -> some View { - VStack(spacing: .small) { - TextField("Event name", text: $viewModel.title) - .title(.bold) - .focused($focusedField, equals: .title) - .onSurfacePrimaryForeground() - .padding(.bottom, .xxxSmall) - .padding(.horizontal, .small) + @ViewBuilder + private func content() -> some View { + VStack(spacing: .small) { + TextField("Event name", text: $viewModel.title) + .title(.bold) + .focused($focusedField, equals: .title) + .onSurfacePrimaryForeground() + .padding(.bottom, .xxxSmall) + .padding(.horizontal, .small) - #if !os(watchOS) - textEditor - #endif + #if !os(watchOS) + textEditor + #endif - calendarButtons + calendarButtons - allDayEvent + allDayEvent - locationView + locationView - alarmView + alarmView - membersView + membersView - repitView - } - .padding(.horizontal, .small) - .padding(.vertical, .medium) + repitView } + .padding(.horizontal, .small) + .padding(.vertical, .medium) + } - var allDayEvent: some View { - Surface { - viewModel.isAllDay.toggle() - } label: { - HStack { - Text("All-day event") - .headline(.semibold) - .foregroundColor(.onSurfacePrimary) - .padding(.leading, .xxxSmall) + var allDayEvent: some View { + Surface { + viewModel.isAllDay.toggle() + } label: { + HStack { + Text("All-day event") + .headline(.semibold) + .foregroundColor(.onSurfacePrimary) + .padding(.leading, .xxxSmall) - Spacer() + Spacer() - Toggle(isOn: $viewModel.isAllDay) {} - .labelsHidden() - } + Toggle(isOn: $viewModel.isAllDay) {} + .labelsHidden() } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.init(horizontal: .xSmall, vertical: .xSmall)) - .controlRadius(.large) } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.init(horizontal: .xSmall, vertical: .xSmall)) + .controlRadius(.large) + } - #if !os(watchOS) - var textEditor: some View { - VStack(spacing: 2) { - TextEditor(text: $viewModel.note) - .onSurfacePrimaryForeground() - .padding(.horizontal, .xSmall) - .padding(.vertical, .xxSmall) - .focused($focusedField, equals: .note) - .body(.medium) - .scrollContentBackground(.hidden) - .background { - #if os(iOS) - RoundedRectangleCorner(radius: 4, corners: [.bottomLeft, .bottomRight]) - .fillSurfaceSecondary() - .overlay(alignment: .topLeading) { - if viewModel.note.isEmpty { - Text("Note") - .body(.medium) - .onSurfaceTertiaryForeground() - .padding(.small) - } - } - #else - RoundedRectangle(cornerRadius: .small) - .fillSurfaceSecondary() - .overlay(alignment: .topLeading) { - if viewModel.note.isEmpty { - Text("Note") - .body(.medium) - .onSurfaceTertiaryForeground() - .padding(.small) - } - } - #endif + #if !os(watchOS) + var textEditor: some View { + VStack(spacing: 2) { + TextEditor(text: $viewModel.note) + .onSurfacePrimaryForeground() + .padding(.horizontal, .xSmall) + .padding(.vertical, .xxSmall) + .focused($focusedField, equals: .note) + .body(.medium) + .scrollContentBackground(.hidden) + .background { + #if os(iOS) + RoundedRectangleCorner(radius: 4, corners: [.bottomLeft, .bottomRight]) + .fillSurfaceSecondary() + .overlay(alignment: .topLeading) { + if viewModel.note.isEmpty { + Text("Note") + .body(.medium) + .onSurfaceTertiaryForeground() + .padding(.small) + } } - .frame(minHeight: 76) - - TextField("URL", text: $viewModel.url) - .focused($focusedField, equals: .url) - .onSurfacePrimaryForeground() - .body(.medium) - .padding(.horizontal, .small) - .padding(.vertical, 18) - .background { - #if os(iOS) - RoundedRectangleCorner(radius: 4, corners: [.topLeft, .topRight]) - .fillSurfaceSecondary() - #else - RoundedRectangle(cornerRadius: .small) - .fillSurfaceSecondary() - #endif + #else + RoundedRectangle(cornerRadius: .small) + .fillSurfaceSecondary() + .overlay(alignment: .topLeading) { + if viewModel.note.isEmpty { + Text("Note") + .body(.medium) + .onSurfaceTertiaryForeground() + .padding(.small) + } } + #endif } - .clipShape(RoundedRectangle(cornerRadius: .large, style: .continuous)) - } - #endif - - var repitView: some View { - Group { - if viewModel.repitRule != .never { - Surface { - Row(viewModel.repitRule.title, subtitle: repeatSubtitleText) { - viewModel.present(.repeat) - } leading: { - IconDeprecated(.refresh) - .iconColor(.onSurfacePrimary) - } - .rowClearButton(style: .onSurface) { - viewModel.repitRule = .never - viewModel.repitEndRule = .never - } - .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) - } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.zero) - .controlRadius(.large) + .frame(minHeight: 76) + + TextField("URL", text: $viewModel.url) + .focused($focusedField, equals: .url) + .onSurfacePrimaryForeground() + .body(.medium) + .padding(.horizontal, .small) + .padding(.vertical, 18) + .background { + #if os(iOS) + RoundedRectangleCorner(radius: 4, corners: [.topLeft, .topRight]) + .fillSurfaceSecondary() + #else + RoundedRectangle(cornerRadius: .small) + .fillSurfaceSecondary() + #endif } - } } + .clipShape(RoundedRectangle(cornerRadius: .large, style: .continuous)) + } + #endif - var membersView: some View { - Group { - if !viewModel.members.isEmpty { - Surface { - VStack(spacing: .zero) { - ForEach(viewModel.members, id: \.self) { email in - Row(email) { - viewModel.present(.invites) - } leading: { - IconDeprecated(.user) - .iconColor(.onSurfacePrimary) - } - .rowClearButton(style: .onSurface) { - viewModel.members.remove(email) - } - .rowContentMargins(.small) - .overlay(alignment: .bottomLeading) { - Rectangle() - .fillSurfaceSecondary() - .padding(.leading, 56) - .frame(height: 1) - } - } - } + var repitView: some View { + Group { + if viewModel.repitRule != .never { + Surface { + Row(viewModel.repitRule.title, subtitle: repeatSubtitleText) { + viewModel.present(.repeat) + } leading: { + IconDeprecated(.refresh) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.repitRule = .never + viewModel.repitEndRule = .never } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.zero) - .controlRadius(.large) + .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.zero) + .controlRadius(.large) } } + } - @ViewBuilder - var alarmView: some View { - Group { - if !viewModel.alarms.isEmpty { - Surface { - VStack(spacing: .zero) { - ForEach(viewModel.alarms) { alarm in - Row(alarm.title) { - viewModel.present(.alarm) - } leading: { - IconDeprecated(.bell) - .iconColor(.onSurfacePrimary) - } - .rowClearButton(style: .onSurface) { - viewModel.alarms.remove(alarm) - } - .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) - .overlay(alignment: .bottomLeading) { - Rectangle() - .fillSurfaceSecondary() - .padding(.leading, 56) - .frame(height: 1) - } + var membersView: some View { + Group { + if !viewModel.members.isEmpty { + Surface { + VStack(spacing: .zero) { + ForEach(viewModel.members, id: \.self) { email in + Row(email) { + viewModel.present(.invites) + } leading: { + IconDeprecated(.user) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.members.remove(email) + } + .rowContentMargins(.small) + .overlay(alignment: .bottomLeading) { + Rectangle() + .fillSurfaceSecondary() + .padding(.leading, 56) + .frame(height: 1) } } } - .surfaceBorderColor(Color.surfaceSecondary) - .surfaceBorderWidth(1) - .surfaceContentMargins(.zero) - .controlRadius(.large) } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.zero) + .controlRadius(.large) } } + } - @ViewBuilder - var locationView: some View { - if viewModel.locationName != nil || viewModel.location != nil { + @ViewBuilder + var alarmView: some View { + Group { + if !viewModel.alarms.isEmpty { Surface { VStack(spacing: .zero) { - if let locationName = viewModel.locationName { - VStack(spacing: .xxSmall) { - Row(locationName) { - viewModel.present(.location) - } leading: { - IconDeprecated(.mapPin) - .iconColor(.onSurfacePrimary) - } - .rowClearButton(style: .onSurface) { - viewModel.locationName = nil - viewModel.location = nil - } - .rowContentMargins(.init(horizontal: .small, vertical: .xSmall)) + ForEach(viewModel.alarms) { alarm in + Row(alarm.title) { + viewModel.present(.alarm) + } leading: { + IconDeprecated(.bell) + .iconColor(.onSurfacePrimary) } - } - - if let location = viewModel.location { - let region = MKCoordinateRegion(center: location, latitudinalMeters: 10000, longitudinalMeters: 10000) - let annotations = [MapPreviewPoint(name: "\(viewModel.locationName ?? "")", coordinate: location)] - Map(coordinateRegion: .constant(region), annotationItems: annotations) { - MapMarker(coordinate: $0.coordinate) + .rowClearButton(style: .onSurface) { + viewModel.alarms.remove(alarm) } - .frame(height: 130) - .cornerRadius(.small) - .padding(.horizontal, .xxSmall) - .padding(.bottom, .xxSmall) - .onTapGesture { - focusedField = nil - viewModel.present(.location) + .surfaceContentMargins(.init(horizontal: .small, vertical: .medium)) + .overlay(alignment: .bottomLeading) { + Rectangle() + .fillSurfaceSecondary() + .padding(.leading, 56) + .frame(height: 1) } } } @@ -336,174 +290,220 @@ import SwiftUI .controlRadius(.large) } } + } + + @ViewBuilder + var locationView: some View { + if viewModel.locationName != nil || viewModel.location != nil { + Surface { + VStack(spacing: .zero) { + if let locationName = viewModel.locationName { + VStack(spacing: .xxSmall) { + Row(locationName) { + viewModel.present(.location) + } leading: { + IconDeprecated(.mapPin) + .iconColor(.onSurfacePrimary) + } + .rowClearButton(style: .onSurface) { + viewModel.locationName = nil + viewModel.location = nil + } + .rowContentMargins(.init(horizontal: .small, vertical: .xSmall)) + } + } - var repeatSubtitleText: String? { - switch viewModel.repitEndRule { - case .never: - return nil - case let .occurrenceCount(count): - return count > 1 ? "With \(count) repetitions" : "With 1 repetition" - case let .endDate(date): - return "Until \(date.formatted(date: .long, time: .omitted))" + if let location = viewModel.location { + let region = MKCoordinateRegion(center: location, latitudinalMeters: 10000, longitudinalMeters: 10000) + let annotations = [MapPreviewPoint(name: "\(viewModel.locationName ?? "")", coordinate: location)] + Map(coordinateRegion: .constant(region), annotationItems: annotations) { + MapMarker(coordinate: $0.coordinate) + } + .frame(height: 130) + .cornerRadius(.small) + .padding(.horizontal, .xxSmall) + .padding(.bottom, .xxSmall) + .onTapGesture { + focusedField = nil + viewModel.present(.location) + } + } + } } + .surfaceBorderColor(Color.surfaceSecondary) + .surfaceBorderWidth(1) + .surfaceContentMargins(.zero) + .controlRadius(.large) } + } - var calendarButtons: some View { - HStack(spacing: .small) { - Button { - focusedField = nil - viewModel.present(.startTime) - } label: { - VStack(alignment: .leading, spacing: .xxxSmall) { - Text("Starts") - .onSurfaceSecondaryForeground() - .subheadline(.semibold) - - Text(startDateText) + var repeatSubtitleText: String? { + switch viewModel.repitEndRule { + case .never: + nil + case let .occurrenceCount(count): + count > 1 ? "With \(count) repetitions" : "With 1 repetition" + case let .endDate(date): + "Until \(date.formatted(date: .long, time: .omitted))" + } + } + + var calendarButtons: some View { + HStack(spacing: .small) { + Button { + focusedField = nil + viewModel.present(.startTime) + } label: { + VStack(alignment: .leading, spacing: .xxxSmall) { + Text("Starts") + .onSurfaceSecondaryForeground() + .subheadline(.semibold) + + Text(startDateText) + .onSurfacePrimaryForeground() + .headline(.semibold) + + if !isCurrentYearEvent { + Text(viewModel.dateStart.formatted(.dateTime.year())) .onSurfacePrimaryForeground() .headline(.semibold) - - if !isCurrentYearEvent { - Text(viewModel.dateStart.formatted(.dateTime.year())) - .onSurfacePrimaryForeground() - .headline(.semibold) - } - } - .padding(.small) - .hLeading() - .background { - RoundedRectangle(cornerRadius: .large, style: .continuous) - .fillSurfaceSecondary() } } - .buttonStyle(.scale) + .padding(.small) + .hLeading() + .background { + RoundedRectangle(cornerRadius: .large, style: .continuous) + .fillSurfaceSecondary() + } + } + .buttonStyle(.scale) - Button { - focusedField = nil - viewModel.present(.endTime) - } label: { - VStack(alignment: .leading, spacing: .xxxSmall) { - Text("Ended") - .onSurfaceSecondaryForeground() - .subheadline(.semibold) - - Text(endDateText) + Button { + focusedField = nil + viewModel.present(.endTime) + } label: { + VStack(alignment: .leading, spacing: .xxxSmall) { + Text("Ended") + .onSurfaceSecondaryForeground() + .subheadline(.semibold) + + Text(endDateText) + .onSurfacePrimaryForeground() + .headline(.semibold) + + if !isCurrentYearEvent { + Text(viewModel.dateEnd.formatted(.dateTime.year())) .onSurfacePrimaryForeground() .headline(.semibold) - - if !isCurrentYearEvent { - Text(viewModel.dateEnd.formatted(.dateTime.year())) - .onSurfacePrimaryForeground() - .headline(.semibold) - } - } - .padding(.small) - .hLeading() - .background { - RoundedRectangle(cornerRadius: .large, style: .continuous) - .fillSurfaceSecondary() } } - .buttonStyle(.scale) + .padding(.small) + .hLeading() + .background { + RoundedRectangle(cornerRadius: .large, style: .continuous) + .fillSurfaceSecondary() + } } + .buttonStyle(.scale) } + } - var isCurrentYearEvent: Bool { - Calendar.current.component(.year, from: viewModel.dateStart) == Calendar.current.component(.year, from: Date()) && Calendar.current.component(.year, from: viewModel.dateEnd) == Calendar.current.component(.year, from: Date()) - } + var isCurrentYearEvent: Bool { + Calendar.current.component(.year, from: viewModel.dateStart) == Calendar.current.component(.year, from: Date()) && Calendar.current.component(.year, from: viewModel.dateEnd) == Calendar.current.component(.year, from: Date()) + } - var startDateText: String { - if Calendar.current.isDateInToday(viewModel.dateStart) { - return "Today \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInTomorrow(viewModel.dateStart) { - return "Tomorrow \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInYesterday(viewModel.dateStart) { - return "Yesterday \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } else { - return "\(viewModel.dateStart.formatted(.dateTime.day().month())) \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" - } + var startDateText: String { + if Calendar.current.isDateInToday(viewModel.dateStart) { + "Today \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInTomorrow(viewModel.dateStart) { + "Tomorrow \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInYesterday(viewModel.dateStart) { + "Yesterday \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" + } else { + "\(viewModel.dateStart.formatted(.dateTime.day().month())) \(viewModel.dateStart.formatted(date: .omitted, time: .shortened))" } + } - var endDateText: String { - if Calendar.current.isDateInToday(viewModel.dateEnd) { - return "Today \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInTomorrow(viewModel.dateEnd) { - return "Tomorrow \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } else if Calendar.current.isDateInYesterday(viewModel.dateEnd) { - return "Yesterday \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } else { - return "\(viewModel.dateEnd.formatted(.dateTime.day().month())) \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" - } + var endDateText: String { + if Calendar.current.isDateInToday(viewModel.dateEnd) { + "Today \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInTomorrow(viewModel.dateEnd) { + "Tomorrow \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } else if Calendar.current.isDateInYesterday(viewModel.dateEnd) { + "Yesterday \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" + } else { + "\(viewModel.dateEnd.formatted(.dateTime.day().month())) \(viewModel.dateEnd.formatted(date: .omitted, time: .shortened))" } + } - var bottomBar: some View { - HStack(spacing: .medium) { - Button { - Task { - focusedField = nil - viewModel.present(.location) - } - } label: { - if viewModel.isFetchUpdatePositon { - ProgressView() - } else { - IconDeprecated(.mapPin) - } + var bottomBar: some View { + HStack(spacing: .medium) { + Button { + Task { + focusedField = nil + viewModel.present(.location) } - .disabled(viewModel.isFetchUpdatePositon) - - Button { viewModel.present(.alarm) } label: { - IconDeprecated(.bell) + } label: { + if viewModel.isFetchUpdatePositon { + ProgressView() + } else { + IconDeprecated(.mapPin) } + } + .disabled(viewModel.isFetchUpdatePositon) - Button { viewModel.present(.repeat) } label: { - IconDeprecated(.refresh) - } + Button { viewModel.present(.alarm) } label: { + IconDeprecated(.bell) + } - /* - Button { viewModel.present(.attachment) } label: { - IconDeprecated(.moreHorizontal) - } - */ + Button { viewModel.present(.repeat) } label: { + IconDeprecated(.refresh) + } - Spacer() + /* + Button { viewModel.present(.attachment) } label: { + IconDeprecated(.moreHorizontal) + } + */ - Button { viewModel.present(.invites) } label: { - IconDeprecated(.userPlus) - } + Spacer() + + Button { viewModel.present(.invites) } label: { + IconDeprecated(.userPlus) + } // Icon.Solid.UserInterface.plusCrFr // .renderingMode(.template) - } - .buttonStyle(.scale) - .padding(.horizontal, .medium) - .padding(.vertical, 20) - .onSurfaceSecondaryForeground() - #if !os(watchOS) - .background(.ultraThinMaterial) - #endif - .overlay(alignment: .top) { - Rectangle() - .fill(Color.onSurfacePrimary.opacity(0.05)) - .frame(height: 1) - } } - - @ViewBuilder - private func placeholder() -> some View {} + .buttonStyle(.scale) + .padding(.horizontal, .medium) + .padding(.vertical, 20) + .onSurfaceSecondaryForeground() + #if !os(watchOS) + .background(.ultraThinMaterial) + #endif + .overlay(alignment: .top) { + Rectangle() + .fill(Color.onSurfacePrimary.opacity(0.05)) + .frame(height: 1) + } } - extension CreateEventView { - enum FocusField: Hashable { - case title - case note - case url - } + @ViewBuilder + private func placeholder() -> some View {} +} + +extension CreateEventView { + enum FocusField: Hashable { + case title + case note + case url } +} - struct CreateEventView_Previews: PreviewProvider { - static var previews: some View { - CreateEventView() - } +struct CreateEventView_Previews: PreviewProvider { + static var previews: some View { + CreateEventView() } +} #endif diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift index 004698e..0aacd16 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewModel.swift @@ -4,7 +4,7 @@ // #if canImport(EventKit) - @preconcurrency import EventKit +@preconcurrency import EventKit #endif import Factory import OversizeCalendarService @@ -14,173 +14,173 @@ import OversizeModels import SwiftUI #if !os(tvOS) - public enum CreateEventType: Equatable, @unchecked Sendable { - case new(Date?, calendar: EKCalendar?) - case update(EKEvent) +public enum CreateEventType: Equatable, @unchecked Sendable { + case new(Date?, calendar: EKCalendar?) + case update(EKEvent) +} + +public class CreateEventViewModel: ObservableObject, @unchecked Sendable { + @Injected(\.calendarService) private var calendarService: CalendarService + @Injected(\.locationService) private var locationService: LocationServiceProtocol + + @Published var state = CreateEventViewModelState.initial + @Published var sheet: CreateEventViewModel.Sheet? = nil + @Published var isFetchUpdatePositon: Bool = .init(false) + + @Published var title: String = .init() + @Published var note: String = .init() + @Published var url: String = .init() + @Published var dateStart: Date = .init() + @Published var dateEnd: Date = .init().halfHour + @Published var isAllDay: Bool = .init(false) + @Published var calendar: EKCalendar? + @Published var calendars: [EKCalendar] = .init() + @Published var sourses: [EKSource] = .init() + @Published var locationName: String? + @Published var location: CLLocationCoordinate2D? + @Published var repitRule: CalendarEventRecurrenceRules = .never + @Published var repitEndRule: CalendarEventEndRecurrenceRules = .never + @Published var alarms: [CalendarAlertsTimes] = .init() + @Published var members: [String] = .init() + @Published var span: EKSpan? + + let type: CreateEventType + + var isLocationSelected: Bool { + location != nil } - public class CreateEventViewModel: ObservableObject, @unchecked Sendable { - @Injected(\.calendarService) private var calendarService: CalendarService - @Injected(\.locationService) private var locationService: LocationServiceProtocol - - @Published var state = CreateEventViewModelState.initial - @Published var sheet: CreateEventViewModel.Sheet? = nil - @Published var isFetchUpdatePositon: Bool = .init(false) - - @Published var title: String = .init() - @Published var note: String = .init() - @Published var url: String = .init() - @Published var dateStart: Date = .init() - @Published var dateEnd: Date = .init().halfHour - @Published var isAllDay: Bool = .init(false) - @Published var calendar: EKCalendar? - @Published var calendars: [EKCalendar] = .init() - @Published var sourses: [EKSource] = .init() - @Published var locationName: String? - @Published var location: CLLocationCoordinate2D? - @Published var repitRule: CalendarEventRecurrenceRules = .never - @Published var repitEndRule: CalendarEventEndRecurrenceRules = .never - @Published var alarms: [CalendarAlertsTimes] = .init() - @Published var members: [String] = .init() - @Published var span: EKSpan? - - let type: CreateEventType - - var isLocationSelected: Bool { - location != nil - } - - public init(_ type: CreateEventType) { - self.type = type - setEvent(type: type) - } + public init(_ type: CreateEventType) { + self.type = type + setEvent(type: type) + } - func setEvent(type: CreateEventType) { - switch type { - case let .new(date, calendar): - if let date { - dateStart = date - dateEnd = date.halfHour - } - if let calendar { - self.calendar = calendar - } - case let .update(event): - title = event.title - note = event.notes ?? "" - url = event.url?.absoluteString ?? "" - dateStart = event.startDate - dateEnd = event.endDate - isAllDay = event.isAllDay - calendar = event.calendar - locationName = event.location - if let coordinate = event.structuredLocation?.geoLocation?.coordinate { - location = coordinate - } - if let rule = event.recurrenceRules?.first { - repitRule = rule.calendarRecurrenceRule - repitEndRule = rule.recurrenceEnd?.calendarEndRecurrenceRule ?? .never - } - if let eventAlarms = event.alarms { - alarms = eventAlarms.compactMap { $0.calendarAlert } - } - if let attendees = event.attendees { - members = attendees.compactMap { $0.url.absoluteString } - } + func setEvent(type: CreateEventType) { + switch type { + case let .new(date, calendar): + if let date { + dateStart = date + dateEnd = date.halfHour } - } - - func fetchData() async { - state = .loading - async let calendarsResult = await calendarService.fetchCalendars() - switch await calendarsResult { - case let .success(data): - log("✅ EKCalendars fetched") - calendars = data - case let .failure(error): - log("❌ EKCalendars not fetched (\(error.title))") - state = .error(error) + if let calendar { + self.calendar = calendar } - async let soursesResult = await calendarService.fetchSourses() - switch await soursesResult { - case let .success(data): - log("✅ EKSource fetched") - sourses = data - case let .failure(error): - log("❌ EKSource not fetched (\(error.title))") - state = .error(error) + case let .update(event): + title = event.title + note = event.notes ?? "" + url = event.url?.absoluteString ?? "" + dateStart = event.startDate + dateEnd = event.endDate + isAllDay = event.isAllDay + calendar = event.calendar + locationName = event.location + if let coordinate = event.structuredLocation?.geoLocation?.coordinate { + location = coordinate } - if case let .new(_, calendar) = type, calendar == nil { - let result = await calendarService.fetchDefaultCalendar() - switch result { - case let .success(calendar): - self.calendar = calendar - case let .failure(error): - log("❌ Default calendar not fetched (\(error.title))") - } + if let rule = event.recurrenceRules?.first { + repitRule = rule.calendarRecurrenceRule + repitEndRule = rule.recurrenceEnd?.calendarEndRecurrenceRule ?? .never } - } - - func save() async -> Result { - var oldEvent: EKEvent? - - if case let .update(event) = type { - oldEvent = event + if let eventAlarms = event.alarms { + alarms = eventAlarms.compactMap { $0.calendarAlert } } + if let attendees = event.attendees { + members = attendees.compactMap { $0.url.absoluteString } + } + } + } - let result = await calendarService.createEvent( - event: oldEvent, - title: title, - notes: note, - startDate: dateStart, - endDate: dateEnd, - calendar: calendar, - isAllDay: isAllDay, - location: locationName, - structuredLocation: getEKStructuredLocation(), - alarms: alarms, - url: URL(string: url), - memberEmails: members, - recurrenceRules: repitRule, - recurrenceEndRules: repitEndRule, - span: span ?? .thisEvent - ) + func fetchData() async { + state = .loading + async let calendarsResult = await calendarService.fetchCalendars() + switch await calendarsResult { + case let .success(data): + log("✅ EKCalendars fetched") + calendars = data + case let .failure(error): + log("❌ EKCalendars not fetched (\(error.title))") + state = .error(error) + } + async let soursesResult = await calendarService.fetchSourses() + switch await soursesResult { + case let .success(data): + log("✅ EKSource fetched") + sourses = data + case let .failure(error): + log("❌ EKSource not fetched (\(error.title))") + state = .error(error) + } + if case let .new(_, calendar) = type, calendar == nil { + let result = await calendarService.fetchDefaultCalendar() switch result { - case let .success(data): - log("✅ EKEvent saved") - return .success(data) + case let .success(calendar): + self.calendar = calendar case let .failure(error): - log("❌ EKEvent not saved (\(error.title))") - return .failure(error) + log("❌ Default calendar not fetched (\(error.title))") } } + } - func getEKStructuredLocation() -> EKStructuredLocation? { - if let location { - let structuredLocation: EKStructuredLocation? - let location = CLLocation(latitude: location.latitude, longitude: location.longitude) - structuredLocation = EKStructuredLocation(title: locationName ?? "") // same title with ekEvent.location - structuredLocation?.geoLocation = location - return structuredLocation - } else { - return nil - } + func save() async -> Result { + var oldEvent: EKEvent? + + if case let .update(event) = type { + oldEvent = event } - func updateCurrentPosition() async throws { - isFetchUpdatePositon = true - let currentPosition = try await locationService.currentLocation() - guard let newLocation = currentPosition else { return } - location = newLocation - log("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") - isFetchUpdatePositon = false + let result = await calendarService.createEvent( + event: oldEvent, + title: title, + notes: note, + startDate: dateStart, + endDate: dateEnd, + calendar: calendar, + isAllDay: isAllDay, + location: locationName, + structuredLocation: getEKStructuredLocation(), + alarms: alarms, + url: URL(string: url), + memberEmails: members, + recurrenceRules: repitRule, + recurrenceEndRules: repitEndRule, + span: span ?? .thisEvent + ) + switch result { + case let .success(data): + log("✅ EKEvent saved") + return .success(data) + case let .failure(error): + log("❌ EKEvent not saved (\(error.title))") + return .failure(error) + } + } + + func getEKStructuredLocation() -> EKStructuredLocation? { + if let location { + let structuredLocation: EKStructuredLocation? + let location = CLLocation(latitude: location.latitude, longitude: location.longitude) + structuredLocation = EKStructuredLocation(title: locationName ?? "") // same title with ekEvent.location + structuredLocation?.geoLocation = location + return structuredLocation + } else { + return nil } } - public enum CreateEventViewModelState { - case initial - case loading - case result([EKEvent]) - case error(AppError) + func updateCurrentPosition() async throws { + isFetchUpdatePositon = true + let currentPosition = try await locationService.currentLocation() + guard let newLocation = currentPosition else { return } + location = newLocation + log("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") + isFetchUpdatePositon = false } +} + +public enum CreateEventViewModelState { + case initial + case loading + case result([EKEvent]) + case error(AppError) +} #endif diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift index beb178c..8a47b98 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/CreateEventViewSheet.swift @@ -4,7 +4,7 @@ // #if canImport(EventKit) - import EventKit +import EventKit #endif import OversizeComponents import OversizeContactsKit @@ -13,109 +13,109 @@ import OversizeUI import SwiftUI #if !os(tvOS) - public extension CreateEventViewModel { - func present(_ sheet: CreateEventViewModel.Sheet) { - self.sheet = sheet - } +public extension CreateEventViewModel { + func present(_ sheet: CreateEventViewModel.Sheet) { + self.sheet = sheet + } - func close() { - sheet = nil - } + func close() { + sheet = nil } +} - public extension CreateEventViewModel { - enum Sheet { - case startTime - case endTime - case attachment - case calendar - case location - case `repeat` - case alarm - case invites - case span - } // attachment, alert, invitees +public extension CreateEventViewModel { + enum Sheet { + case startTime + case endTime + case attachment + case calendar + case location + case `repeat` + case alarm + case invites + case span + } // attachment, alert, invitees +} + +extension CreateEventViewModel.Sheet: Identifiable { + public var id: String { + switch self { + case .startTime: + "startTime" + case .endTime: + "endTime" + case .attachment: + "attachment" + case .calendar: + "calendar" + case .location: + "location" + case .repeat: + "repeat" + case .alarm: + "alarm" + case .invites: + "alarm" + case .span: + "span" + } } +} - extension CreateEventViewModel.Sheet: Identifiable { - public var id: String { - switch self { +public extension CreateEventView { + func resolveSheet(sheet: CreateEventViewModel.Sheet) -> some View { + Group { + switch sheet { case .startTime: - return "startTime" + #if os(iOS) + DatePickerSheet(title: "Starts time", selection: $viewModel.dateStart) + .onDisappear { + if viewModel.dateStart > viewModel.dateEnd { + viewModel.dateEnd = viewModel.dateStart.halfHour + } + } + .presentationDetents([.height(500)]) + #else + EmptyView() + #endif case .endTime: - return "endTime" + #if os(iOS) + DatePickerSheet(title: "Ends time", selection: $viewModel.dateEnd) + .datePickerMinimumDate(viewModel.dateStart.minute) + .presentationDetents([.height(500)]) + #else + EmptyView() + #endif case .attachment: - return "attachment" + AttachmentView() + .presentationDetents([.height(270)]) case .calendar: - return "calendar" + CalendarPicker(selection: $viewModel.calendar, calendars: viewModel.calendars, sourses: viewModel.sourses) + .presentationDetents([.large]) case .location: - return "location" + #if !os(watchOS) + AddressPicker(address: $viewModel.locationName, location: $viewModel.location) + .interactiveDismissDisabled(true) + .presentationDetents([.large]) + #else + EmptyView() + #endif case .repeat: - return "repeat" + RepeatPicker(selectionRule: $viewModel.repitRule, selectionEndRule: $viewModel.repitEndRule) case .alarm: - return "alarm" + AlarmPicker(selection: $viewModel.alarms) + .presentationDetents([.height(630), .large]) + .presentationDragIndicator(.hidden) case .invites: - return "alarm" + EmailPickerView(selection: $viewModel.members) + .presentationDetents([.large]) + .interactiveDismissDisabled(true) case .span: - return "span" - } - } - } - - public extension CreateEventView { - func resolveSheet(sheet: CreateEventViewModel.Sheet) -> some View { - Group { - switch sheet { - case .startTime: - #if os(iOS) - DatePickerSheet(title: "Starts time", selection: $viewModel.dateStart) - .onDisappear { - if viewModel.dateStart > viewModel.dateEnd { - viewModel.dateEnd = viewModel.dateStart.halfHour - } - } - .presentationDetents([.height(500)]) - #else - EmptyView() - #endif - case .endTime: - #if os(iOS) - DatePickerSheet(title: "Ends time", selection: $viewModel.dateEnd) - .datePickerMinimumDate(viewModel.dateStart.minute) - .presentationDetents([.height(500)]) - #else - EmptyView() - #endif - case .attachment: - AttachmentView() - .presentationDetents([.height(270)]) - case .calendar: - CalendarPicker(selection: $viewModel.calendar, calendars: viewModel.calendars, sourses: viewModel.sourses) - .presentationDetents([.large]) - case .location: - #if !os(watchOS) - AddressPicker(address: $viewModel.locationName, location: $viewModel.location) - .interactiveDismissDisabled(true) - .presentationDetents([.large]) - #else - EmptyView() - #endif - case .repeat: - RepeatPicker(selectionRule: $viewModel.repitRule, selectionEndRule: $viewModel.repitEndRule) - case .alarm: - AlarmPicker(selection: $viewModel.alarms) - .presentationDetents([.height(630), .large]) - .presentationDragIndicator(.hidden) - case .invites: - EmailPickerView(selection: $viewModel.members) - .presentationDetents([.large]) - .interactiveDismissDisabled(true) - case .span: - SaveForView(selection: $viewModel.span) - .presentationDetents([.height(270)]) - } + SaveForView(selection: $viewModel.span) + .presentationDetents([.height(270)]) } - .systemServices() } + .systemServices() } +} #endif diff --git a/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift b/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift index 676b9a5..8e0c794 100644 --- a/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift +++ b/Sources/OversizeCalendarKit/CreateEventScreen/SaveForView/SaveForView.swift @@ -4,49 +4,49 @@ // #if canImport(EventKit) - import EventKit +import EventKit #endif import OversizeUI import SwiftUI #if !os(tvOS) - public struct SaveForView: View { - @Environment(\.dismiss) var dismiss - @Binding private var span: EKSpan? +public struct SaveForView: View { + @Environment(\.dismiss) var dismiss + @Binding private var span: EKSpan? - public init(selection: Binding) { - _span = selection - } + public init(selection: Binding) { + _span = selection + } - public var body: some View { - PageView("This is repeating event") { - SectionView { - VStack(spacing: .zero) { - Row("Save for this event only") { - span = .thisEvent - dismiss() - } leading: { - Image.Date.calendar - .renderingMode(.template) - .foregroundColor(.onSurfacePrimary) - } + public var body: some View { + PageView("This is repeating event") { + SectionView { + VStack(spacing: .zero) { + Row("Save for this event only") { + span = .thisEvent + dismiss() + } leading: { + Image.Date.calendar + .renderingMode(.template) + .foregroundColor(.onSurfacePrimary) + } - Row("Save for feature events") { - span = .futureEvents - dismiss() - } leading: { - Image.Base.calendar - .renderingMode(.template) - .foregroundColor(.onSurfacePrimary) - } + Row("Save for feature events") { + span = .futureEvents + dismiss() + } leading: { + Image.Base.calendar + .renderingMode(.template) + .foregroundColor(.onSurfacePrimary) } } - .surfaceContentRowMargins() - } - .backgroundSecondary() - .leadingBar { - BarButton(.close) } + .surfaceContentRowMargins() + } + .backgroundSecondary() + .leadingBar { + BarButton(.close) } } +} #endif diff --git a/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift b/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift index f31672c..6d97495 100644 --- a/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift +++ b/Sources/OversizeCalendarKit/Pickers/AlertPicker.swift @@ -4,53 +4,53 @@ // #if canImport(EventKit) - import EventKit +import EventKit #endif import OversizeCalendarService import OversizeUI import SwiftUI #if !os(tvOS) - public struct AlarmPicker: View { - @Environment(\.dismiss) var dismiss - @Binding private var selection: [CalendarAlertsTimes] - @State private var selectedAlerts: [CalendarAlertsTimes] = [] +public struct AlarmPicker: View { + @Environment(\.dismiss) var dismiss + @Binding private var selection: [CalendarAlertsTimes] + @State private var selectedAlerts: [CalendarAlertsTimes] = [] - public init(selection: Binding<[CalendarAlertsTimes]>) { - _selection = selection - _selectedAlerts = State(wrappedValue: selection.wrappedValue) - } + public init(selection: Binding<[CalendarAlertsTimes]>) { + _selection = selection + _selectedAlerts = State(wrappedValue: selection.wrappedValue) + } - public var body: some View { - PageView("Alarm") { - SectionView { - VStack(spacing: .zero) { - ForEach(CalendarAlertsTimes.allCases) { alert in - Checkbox(alert.title, isOn: .constant((selectedAlerts.first { $0.id == alert.id } != nil) ? true : false)) { - if !selectedAlerts.isEmpty, let _ = selectedAlerts.first(where: { $0.id == alert.id }) { - selectedAlerts.remove(alert) - } else { - selectedAlerts.append(alert) - } + public var body: some View { + PageView("Alarm") { + SectionView { + VStack(spacing: .zero) { + ForEach(CalendarAlertsTimes.allCases) { alert in + Checkbox(alert.title, isOn: .constant((selectedAlerts.first { $0.id == alert.id } != nil) ? true : false)) { + if !selectedAlerts.isEmpty, let _ = selectedAlerts.first(where: { $0.id == alert.id }) { + selectedAlerts.remove(alert) + } else { + selectedAlerts.append(alert) } } } } - .surfaceContentRowMargins() - } - .backgroundSecondary() - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - selection = selectedAlerts - dismiss() - })) - .disabled(selectedAlerts.isEmpty) } + .surfaceContentRowMargins() + } + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + selection = selectedAlerts + dismiss() + })) + .disabled(selectedAlerts.isEmpty) } } +} #endif // struct AlertPicker_Previews: PreviewProvider { diff --git a/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift b/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift index 8244d84..43abc94 100644 --- a/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift +++ b/Sources/OversizeCalendarKit/Pickers/CalendarPicker.swift @@ -4,69 +4,69 @@ // #if canImport(EventKit) - import EventKit +import EventKit #endif import OversizeUI import SwiftUI #if !os(tvOS) - public struct CalendarPicker: View { - @Environment(\.dismiss) var dismiss +public struct CalendarPicker: View { + @Environment(\.dismiss) var dismiss - @Binding private var selection: EKCalendar? + @Binding private var selection: EKCalendar? - private let calendars: [EKCalendar] + private let calendars: [EKCalendar] - private let sourses: [EKSource] + private let sourses: [EKSource] - private let closable: Bool + private let closable: Bool - public init(selection: Binding, calendars: [EKCalendar], sourses: [EKSource], closable: Bool = true) { - _selection = selection - self.calendars = calendars - self.sourses = sourses - self.closable = closable - } + public init(selection: Binding, calendars: [EKCalendar], sourses: [EKSource], closable: Bool = true) { + _selection = selection + self.calendars = calendars + self.sourses = sourses + self.closable = closable + } - public var body: some View { - PageView("Calendar") { - ForEach(sourses, id: \.sourceIdentifier) { source in - let filtredCalendar: [EKCalendar] = calendars.filter { $0.source.sourceIdentifier == source.sourceIdentifier && $0.allowsContentModifications } - if !filtredCalendar.isEmpty { - calendarSection(source: source, calendars: filtredCalendar) - } + public var body: some View { + PageView("Calendar") { + ForEach(sourses, id: \.sourceIdentifier) { source in + let filtredCalendar: [EKCalendar] = calendars.filter { $0.source.sourceIdentifier == source.sourceIdentifier && $0.allowsContentModifications } + if !filtredCalendar.isEmpty { + calendarSection(source: source, calendars: filtredCalendar) } } - .backgroundSecondary() - .leadingBar { - BarButton(closable ? .close : .back) - } } + .backgroundSecondary() + .leadingBar { + BarButton(closable ? .close : .back) + } + } - func calendarSection(source: EKSource, calendars: [EKCalendar]) -> some View { - SectionView(source.title) { - VStack(spacing: .zero) { - ForEach(calendars, id: \.calendarIdentifier) { calendar in - Radio(isOn: selection?.calendarIdentifier == calendar.calendarIdentifier) { - selection = calendar - dismiss() - } label: { - Row(calendar.title) { - Circle() - .fill(Color(calendar.cgColor)) - .frame(width: 16, height: 16) - } + func calendarSection(source: EKSource, calendars: [EKCalendar]) -> some View { + SectionView(source.title) { + VStack(spacing: .zero) { + ForEach(calendars, id: \.calendarIdentifier) { calendar in + Radio(isOn: selection?.calendarIdentifier == calendar.calendarIdentifier) { + selection = calendar + dismiss() + } label: { + Row(calendar.title) { + Circle() + .fill(Color(calendar.cgColor)) + .frame(width: 16, height: 16) } } } } - .surfaceContentRowMargins() } + .surfaceContentRowMargins() } +} - struct CalendarPicker_Previews: PreviewProvider { - static var previews: some View { - CalendarPicker(selection: .constant(nil), calendars: [], sourses: []) - } +struct CalendarPicker_Previews: PreviewProvider { + static var previews: some View { + CalendarPicker(selection: .constant(nil), calendars: [], sourses: []) } +} #endif diff --git a/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift b/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift index 2c32cb8..c55a2b5 100644 --- a/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift +++ b/Sources/OversizeCalendarKit/Pickers/RepeatPicker.swift @@ -4,133 +4,133 @@ // #if canImport(EventKit) - import EventKit +import EventKit #endif import OversizeCalendarService import OversizeUI import SwiftUI #if !os(tvOS) - public struct RepeatPicker: View { - @Environment(\.dismiss) private var dismiss +public struct RepeatPicker: View { + @Environment(\.dismiss) private var dismiss - @Binding private var selectionRule: CalendarEventRecurrenceRules - @Binding private var selectionEndRule: CalendarEventEndRecurrenceRules + @Binding private var selectionRule: CalendarEventRecurrenceRules + @Binding private var selectionEndRule: CalendarEventEndRecurrenceRules - @State private var rule: CalendarEventRecurrenceRules - @State private var endRule: CalendarEventEndRecurrenceRules + @State private var rule: CalendarEventRecurrenceRules + @State private var endRule: CalendarEventEndRecurrenceRules - @State private var endDate: Date = .init() - @State private var repeatCount: String = "1" - @FocusState private var isFocusedRepitCount: Bool + @State private var endDate: Date = .init() + @State private var repeatCount: String = "1" + @FocusState private var isFocusedRepitCount: Bool - public init(selectionRule: Binding, selectionEndRule: Binding) { - _selectionRule = selectionRule - _selectionEndRule = selectionEndRule - _rule = State(wrappedValue: selectionRule.wrappedValue) - _endRule = State(wrappedValue: selectionEndRule.wrappedValue) - } + public init(selectionRule: Binding, selectionEndRule: Binding) { + _selectionRule = selectionRule + _selectionEndRule = selectionEndRule + _rule = State(wrappedValue: selectionRule.wrappedValue) + _endRule = State(wrappedValue: selectionEndRule.wrappedValue) + } - public var body: some View { - ScrollViewReader { scrollView in - PageView("Repeat") { - SectionView { - VStack(spacing: .zero) { - ForEach(CalendarEventRecurrenceRules.allCases) { rule in - Radio(isOn: self.rule.id == rule.id) { - withAnimation { - self.rule = rule - } - } label: { - Row(rule.title) + public var body: some View { + ScrollViewReader { scrollView in + PageView("Repeat") { + SectionView { + VStack(spacing: .zero) { + ForEach(CalendarEventRecurrenceRules.allCases) { rule in + Radio(isOn: self.rule.id == rule.id) { + withAnimation { + self.rule = rule } + } label: { + Row(rule.title) } } } + } - if rule != .never { - SectionView("End Repeat") { - VStack(spacing: .zero) { - ForEach(CalendarEventEndRecurrenceRules.allCases) { rule in - VStack(spacing: .xxSmall) { - Radio(isOn: endRule.id == rule.id) { - endRule = rule - if case .occurrenceCount = endRule { - isFocusedRepitCount = true - scrollView.scrollTo(rule.id) - } - - if case .endDate = endRule { - isFocusedRepitCount = true - scrollView.scrollTo(rule.id) - } - } label: { - Row(rule.title) + if rule != .never { + SectionView("End Repeat") { + VStack(spacing: .zero) { + ForEach(CalendarEventEndRecurrenceRules.allCases) { rule in + VStack(spacing: .xxSmall) { + Radio(isOn: endRule.id == rule.id) { + endRule = rule + if case .occurrenceCount = endRule { + isFocusedRepitCount = true + scrollView.scrollTo(rule.id) } - if endRule.id == rule.id { - repartPicker(rules: rule) - .padding(.horizontal, .medium) - .padding(.bottom, .small) + if case .endDate = endRule { + isFocusedRepitCount = true + scrollView.scrollTo(rule.id) } + } label: { + Row(rule.title) + } + + if endRule.id == rule.id { + repartPicker(rules: rule) + .padding(.horizontal, .medium) + .padding(.bottom, .small) } } } } - .transition(.move(edge: .top)) } + .transition(.move(edge: .top)) } - .backgroundSecondary() - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - selectionRule = rule - selectionEndRule = endRule - dismiss() - })) - .disabled(rule == .never) - } - .surfaceContentRowMargins() } - .presentationDetents(rule == .never ? [.height(630), .large] : [.large]) - .presentationDragIndicator(.hidden) - } - - @ViewBuilder - func repartPicker(rules: CalendarEventEndRecurrenceRules) -> some View { - switch rules { - case .never: - EmptyView() - case .occurrenceCount: - TextField("Number of repetitions", text: Binding(get: { - repeatCount - }, set: { newValue in - repeatCount = newValue - endRule = .occurrenceCount(Int(newValue) ?? 1) + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + selectionRule = rule + selectionEndRule = endRule + dismiss() })) - #if os(iOS) - .keyboardType(.numberPad) - #endif - .textFieldStyle(.default) - .focused($isFocusedRepitCount) - case .endDate: - #if !os(watchOS) - DatePicker("Date", selection: Binding(get: { - endDate - }, set: { newDate in - endDate = newDate - endRule = .endDate(newDate) - })) - #if os(iOS) - .datePickerStyle(.wheel) - #endif - .labelsHidden() - #else - ProgressView() - #endif + .disabled(rule == .never) } + .surfaceContentRowMargins() + } + .presentationDetents(rule == .never ? [.height(630), .large] : [.large]) + .presentationDragIndicator(.hidden) + } + + @ViewBuilder + func repartPicker(rules: CalendarEventEndRecurrenceRules) -> some View { + switch rules { + case .never: + EmptyView() + case .occurrenceCount: + TextField("Number of repetitions", text: Binding(get: { + repeatCount + }, set: { newValue in + repeatCount = newValue + endRule = .occurrenceCount(Int(newValue) ?? 1) + })) + #if os(iOS) + .keyboardType(.numberPad) + #endif + .textFieldStyle(.default) + .focused($isFocusedRepitCount) + case .endDate: + #if !os(watchOS) + DatePicker("Date", selection: Binding(get: { + endDate + }, set: { newDate in + endDate = newDate + endRule = .endDate(newDate) + })) + #if os(iOS) + .datePickerStyle(.wheel) + #endif + .labelsHidden() + #else + ProgressView() + #endif } } +} #endif diff --git a/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift b/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift index 3bca2d1..b2f3793 100644 --- a/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift +++ b/Sources/OversizeContactsKit/AttendeesList/AttendeesView.swift @@ -4,8 +4,8 @@ // #if canImport(Contacts) && canImport(EventKit) - import Contacts - import EventKit +import Contacts +import EventKit #endif import OversizeCalendarService import OversizeContactsService @@ -16,86 +16,86 @@ import OversizeUI import SwiftUI #if !os(tvOS) - public struct AttendeesView: View { - @StateObject var viewModel: AttendeesViewModel - @Environment(\.dismiss) var dismiss +public struct AttendeesView: View { + @StateObject var viewModel: AttendeesViewModel + @Environment(\.dismiss) var dismiss - public init(event: EKEvent) { - _viewModel = StateObject(wrappedValue: AttendeesViewModel(event: event)) - } + public init(event: EKEvent) { + _viewModel = StateObject(wrappedValue: AttendeesViewModel(event: event)) + } - public var body: some View { - PageView("Invitees") { - Group { - switch viewModel.state { - case .initial: - placeholder() - .onAppear { - Task { - await viewModel.fetchData() - } + public var body: some View { + PageView("Invitees") { + Group { + switch viewModel.state { + case .initial: + placeholder() + .onAppear { + Task { + await viewModel.fetchData() } - case .loading: - placeholder() - case let .result(data): - content(data) - case let .error(error): - ErrorView(error) - } + } + case .loading: + placeholder() + case let .result(data): + content(data) + case let .error(error): + ErrorView(error) } } - .leadingBar { - BarButton(.close) - } } + .leadingBar { + BarButton(.close) + } + } - @ViewBuilder - private func content(_: [CNContact]) -> some View { - if let attendees = viewModel.event.attendees { - VStack(spacing: .zero) { - if let organizer = viewModel.event.organizer { - Row(organizer.name ?? organizer.url.absoluteString, subtitle: "Organizer") { - userAvatarView(participant: organizer) - } + @ViewBuilder + private func content(_: [CNContact]) -> some View { + if let attendees = viewModel.event.attendees { + VStack(spacing: .zero) { + if let organizer = viewModel.event.organizer { + Row(organizer.name ?? organizer.url.absoluteString, subtitle: "Organizer") { + userAvatarView(participant: organizer) } + } - ForEach(attendees, id: \.self) { attender in - Row(attender.name ?? attender.url.absoluteString, subtitle: attender.participantRole.title) { - userAvatarView(participant: attender) - } + ForEach(attendees, id: \.self) { attender in + Row(attender.name ?? attender.url.absoluteString, subtitle: attender.participantRole.title) { + userAvatarView(participant: attender) } } } } + } - func userAvatarView(participant: EKParticipant) -> some View { - ZStack(alignment: .bottomTrailing) { - Avatar(firstName: participant.name ?? participant.url.absoluteString) - .controlSize(.regular) + func userAvatarView(participant: EKParticipant) -> some View { + ZStack(alignment: .bottomTrailing) { + Avatar(firstName: participant.name ?? participant.url.absoluteString) + .controlSize(.regular) - ZStack { - Circle() - .fill(participant.color) - .frame(width: 16, height: 16) - .background { - Circle() - .stroke(lineWidth: 4) - .fillBackgroundPrimary() - } - Image(systemName: participant.symbolName) - .onPrimaryForeground() - .font(.system(size: 9, weight: .black)) - } + ZStack { + Circle() + .fill(participant.color) + .frame(width: 16, height: 16) + .background { + Circle() + .stroke(lineWidth: 4) + .fillBackgroundPrimary() + } + Image(systemName: participant.symbolName) + .onPrimaryForeground() + .font(.system(size: 9, weight: .black)) } } + } - @ViewBuilder - private func placeholder() -> some View { - #if os(watchOS) - ProgressView() - #else - LoaderOverlayView() - #endif - } + @ViewBuilder + private func placeholder() -> some View { + #if os(watchOS) + ProgressView() + #else + LoaderOverlayView() + #endif } +} #endif diff --git a/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift b/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift index d597c36..13a66f8 100644 --- a/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift +++ b/Sources/OversizeContactsKit/AttendeesList/AttendeesViewModel.swift @@ -4,8 +4,8 @@ // #if canImport(Contacts) && canImport(EventKit) - @preconcurrency import Contacts - import EventKit +@preconcurrency import Contacts +import EventKit #endif import Factory import OversizeContactsService @@ -14,51 +14,51 @@ import OversizeModels import SwiftUI #if !os(tvOS) - @MainActor - class AttendeesViewModel: ObservableObject { - @Injected(\.contactsService) private var contactsService: ContactsService - @Published var state = AttendeesViewModelState.initial - @Published var searchText: String = .init() +@MainActor +class AttendeesViewModel: ObservableObject { + @Injected(\.contactsService) private var contactsService: ContactsService + @Published var state = AttendeesViewModelState.initial + @Published var searchText: String = .init() - let event: EKEvent + let event: EKEvent - init(event: EKEvent) { - self.event = event - } + init(event: EKEvent) { + self.event = event + } - func fetchData() async { - state = .loading - let _ = await contactsService.requestAccess() + func fetchData() async { + state = .loading + let _ = await contactsService.requestAccess() - let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] - let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) - switch result { - case let .success(data): - log("✅ CNContact fetched") - state = .result(data) - case let .failure(error): - log("❌ CNContact not fetched (\(error.title))") - state = .error(error) - } + let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] + let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) + switch result { + case let .success(data): + log("✅ CNContact fetched") + state = .result(data) + case let .failure(error): + log("❌ CNContact not fetched (\(error.title))") + state = .error(error) } + } - func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { - for contact in contacts where !contact.emailAddresses.isEmpty { - for emailAddress in contact.emailAddresses { - let emailAddressString = emailAddress.value as String - if emailAddressString == email { - return contact - } + func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { + for contact in contacts where !contact.emailAddresses.isEmpty { + for emailAddress in contact.emailAddresses { + let emailAddressString = emailAddress.value as String + if emailAddressString == email { + return contact } } - return nil } + return nil } +} - enum AttendeesViewModelState { - case initial - case loading - case result([CNContact]) - case error(AppError) - } +enum AttendeesViewModelState { + case initial + case loading + case result([CNContact]) + case error(AppError) +} #endif diff --git a/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift b/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift index 11d868c..55f8e21 100644 --- a/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift +++ b/Sources/OversizeContactsKit/ContactsLists/ContactsListsView.swift @@ -4,7 +4,7 @@ // #if canImport(Contacts) - import Contacts +import Contacts #endif import OversizeComponents import OversizeCore @@ -14,84 +14,84 @@ import OversizeUI import SwiftUI #if !os(tvOS) - public struct ContactsListsView: View { - @StateObject var viewModel: ContactsListsViewModel - @Environment(\.dismiss) var dismiss - @Binding private var emails: [String] +public struct ContactsListsView: View { + @StateObject var viewModel: ContactsListsViewModel + @Environment(\.dismiss) var dismiss + @Binding private var emails: [String] - public init(emails: Binding<[String]>) { - _viewModel = StateObject(wrappedValue: ContactsListsViewModel()) - _emails = emails - } + public init(emails: Binding<[String]>) { + _viewModel = StateObject(wrappedValue: ContactsListsViewModel()) + _emails = emails + } - public var body: some View { - PageView("") { - Group { - switch viewModel.state { - case .initial: - placeholder() - case .loading: - placeholder() - case let .result(data): - content(data: data) - case let .error(error): - ErrorView(error) - } + public var body: some View { + PageView("") { + Group { + switch viewModel.state { + case .initial: + placeholder() + case .loading: + placeholder() + case let .result(data): + content(data: data) + case let .error(error): + ErrorView(error) } } - .leadingBar { - BarButton(.close) - } - .task { - await viewModel.fetchData() - } } + .leadingBar { + BarButton(.close) + } + .task { + await viewModel.fetchData() + } + } - @ViewBuilder - private func content(data: [CNContact]) -> some View { - ForEach(emails, id: \.self) { email in - if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { - let emails = contact.emailAddresses - if !emails.isEmpty { - ForEach(emails, id: \.identifier) { email in - emailRow(email: email, contact: contact) - } - } - } else { - Row(email) { - Avatar(firstName: email) + @ViewBuilder + private func content(data: [CNContact]) -> some View { + ForEach(emails, id: \.self) { email in + if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { + let emails = contact.emailAddresses + if !emails.isEmpty { + ForEach(emails, id: \.identifier) { email in + emailRow(email: email, contact: contact) } } + } else { + Row(email) { + Avatar(firstName: email) + } } } + } - @ViewBuilder - private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { - let email = email.value as String - #if os(iOS) - if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) - } - } else { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName) - } - } - #else - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName) - } - #endif + @ViewBuilder + private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { + let email = email.value as String + #if os(iOS) + if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) + } + } else { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName) + } + } + #else + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName) } + #endif + } - @ViewBuilder - private func placeholder() -> some View { - ForEach(emails, id: \.self) { email in - Row(email) { - Avatar(firstName: email) - } + @ViewBuilder + private func placeholder() -> some View { + ForEach(emails, id: \.self) { email in + Row(email) { + Avatar(firstName: email) } } } +} #endif diff --git a/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift b/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift index a7ce31c..85bb498 100644 --- a/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift +++ b/Sources/OversizeContactsKit/ContactsLists/ContactsListsViewModel.swift @@ -4,7 +4,7 @@ // #if canImport(Contacts) - @preconcurrency import Contacts +@preconcurrency import Contacts #endif import Factory import OversizeContactsService @@ -13,47 +13,47 @@ import OversizeModels import SwiftUI #if !os(tvOS) - @MainActor - public class ContactsListsViewModel: ObservableObject { - @Injected(\.contactsService) private var contactsService: ContactsService - @Published var state = ContactsPickerViewModelState.initial - @Published var searchText: String = .init() +@MainActor +public class ContactsListsViewModel: ObservableObject { + @Injected(\.contactsService) private var contactsService: ContactsService + @Published var state = ContactsPickerViewModelState.initial + @Published var searchText: String = .init() - public init() {} + public init() {} - func fetchData() async { - state = .loading - let _ = await contactsService.requestAccess() + func fetchData() async { + state = .loading + let _ = await contactsService.requestAccess() - let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] - let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) - switch result { - case let .success(data): - log("✅ CNContact fetched") - state = .result(data) - case let .failure(error): - log("❌ CNContact not fetched (\(error.title))") - state = .error(error) - } + let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] + let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) + switch result { + case let .success(data): + log("✅ CNContact fetched") + state = .result(data) + case let .failure(error): + log("❌ CNContact not fetched (\(error.title))") + state = .error(error) } + } - func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { - for contact in contacts where !contact.emailAddresses.isEmpty { - for emailAddress in contact.emailAddresses { - let emailAddressString = emailAddress.value as String - if emailAddressString == email { - return contact - } + func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { + for contact in contacts where !contact.emailAddresses.isEmpty { + for emailAddress in contact.emailAddresses { + let emailAddressString = emailAddress.value as String + if emailAddressString == email { + return contact } } - return nil } + return nil } +} - enum ContactsListsViewModelState { - case initial - case loading - case result([CNContact]) - case error(AppError) - } +enum ContactsListsViewModelState { + case initial + case loading + case result([CNContact]) + case error(AppError) +} #endif diff --git a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift index 053b539..f3724d0 100644 --- a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift +++ b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerView.swift @@ -4,7 +4,7 @@ // #if canImport(Contacts) - import Contacts +import Contacts #endif import OversizeKit import OversizeLocalizable @@ -12,227 +12,227 @@ import OversizeUI import SwiftUI #if !os(tvOS) - public struct EmailPickerView: View { - @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel: EmailPickerViewModel +public struct EmailPickerView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel: EmailPickerViewModel - @Binding private var selection: [String] - @State private var selectedEmails: [String] = .init() + @Binding private var selection: [String] + @State private var selectedEmails: [String] = .init() - @FocusState private var isFocusSearth + @FocusState private var isFocusSearth - public init(selection: Binding<[String]>) { - _viewModel = StateObject(wrappedValue: EmailPickerViewModel()) - _selection = selection - } + public init(selection: Binding<[String]>) { + _viewModel = StateObject(wrappedValue: EmailPickerViewModel()) + _selection = selection + } - public var body: some View { - PageView("Add Invitees") { - Group { - switch viewModel.state { - case .initial, .loading: - placeholder() - case let .result(data): - content(data: data) - case let .error(error): - ErrorView(error) - } + public var body: some View { + PageView("Add Invitees") { + Group { + switch viewModel.state { + case .initial, .loading: + placeholder() + case let .result(data): + content(data: data) + case let .error(error): + ErrorView(error) } } - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - onDoneAction() - })) - .disabled(selectedEmails.isEmpty && !viewModel.searchText.isEmail) - } - .topToolbar { - TextField("Email or name", text: $viewModel.searchText) - .textFieldStyle(.default) - .focused($isFocusSearth) - #if os(iOS) - .keyboardType(.emailAddress) - #endif - } - .onAppear { - isFocusSearth = true - } - .task { - await viewModel.fetchData() - } } + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + onDoneAction() + })) + .disabled(selectedEmails.isEmpty && !viewModel.searchText.isEmail) + } + .topToolbar { + TextField("Email or name", text: $viewModel.searchText) + .textFieldStyle(.default) + .focused($isFocusSearth) + #if os(iOS) + .keyboardType(.emailAddress) + #endif + } + .onAppear { + isFocusSearth = true + } + .task { + await viewModel.fetchData() + } + } - @ViewBuilder - private func content(data: [CNContact]) -> some View { - LazyVStack(spacing: .zero) { - newEmailView() - - newSelectedContactsRows(data: data) + @ViewBuilder + private func content(data: [CNContact]) -> some View { + LazyVStack(spacing: .zero) { + newEmailView() - contactsRows(data: data) - } - } + newSelectedContactsRows(data: data) - @ViewBuilder - private func newEmailView() -> some View { - if !viewModel.searchText.isEmpty { - Checkbox( - isOn: .constant(viewModel.searchText.isEmail), - label: { - Row(viewModel.searchText, subtitle: "New member") { - Avatar(firstName: viewModel.searchText) - } - } - ) - .padding(.bottom, .small) - } + contactsRows(data: data) } + } - @ViewBuilder - private func newSelectedContactsRows(data: [CNContact]) -> some View { - if !viewModel.lastSelectedEmails.isEmpty { - HStack(spacing: .zero) { - Text("Latest") - Spacer() - } - .title3() - .onSurfaceSecondaryForeground() - .padding(.vertical, .xxSmall) - .paddingContent(.horizontal) - - ForEach(viewModel.lastSelectedEmails, id: \.self) { email in - if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { - let emails = contact.emailAddresses - if !emails.isEmpty { - ForEach(emails, id: \.identifier) { email in - emailRow(email: email, contact: contact) - } - } - } else { - let isSelected = selectedEmails.contains(email) - Checkbox( - isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), - label: { - Row(email) { - Avatar(firstName: email) - } - } - ) + @ViewBuilder + private func newEmailView() -> some View { + if !viewModel.searchText.isEmpty { + Checkbox( + isOn: .constant(viewModel.searchText.isEmail), + label: { + Row(viewModel.searchText, subtitle: "New member") { + Avatar(firstName: viewModel.searchText) } } - } + ) + .padding(.bottom, .small) } + } - @ViewBuilder - private func contactsRows(data: [CNContact]) -> some View { - if !data.isEmpty { - HStack(spacing: .zero) { - Text("Contacts") - Spacer() - } - .title3() - .onSurfaceSecondaryForeground() - .padding(.vertical, .xxSmall) - .paddingContent(.horizontal) - .padding(.top, viewModel.lastSelectedEmails.isEmpty ? .zero : .small) + @ViewBuilder + private func newSelectedContactsRows(data: [CNContact]) -> some View { + if !viewModel.lastSelectedEmails.isEmpty { + HStack(spacing: .zero) { + Text("Latest") + Spacer() + } + .title3() + .onSurfaceSecondaryForeground() + .padding(.vertical, .xxSmall) + .paddingContent(.horizontal) - ForEach(data, id: \.identifier) { contact in + ForEach(viewModel.lastSelectedEmails, id: \.self) { email in + if let contact = viewModel.getContactFromEmail(email: email, contacts: data) { let emails = contact.emailAddresses if !emails.isEmpty { ForEach(emails, id: \.identifier) { email in emailRow(email: email, contact: contact) } } + } else { + let isSelected = selectedEmails.contains(email) + Checkbox( + isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), + label: { + Row(email) { + Avatar(firstName: email) + } + } + ) } } } + } - @ViewBuilder - private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { - let email = email.value as String - let isSelected = selectedEmails.contains(email) - #if os(iOS) - if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { - Checkbox(isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), label: { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) - } - - }) - } else { - Checkbox(isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), label: { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName) - } - - }) - } - #else - Checkbox(isOn: Binding( - get: { isSelected }, - set: { _ in onContactClick(email: email) } - ), label: { - Row(contact.givenName + " " + contact.familyName, subtitle: email) { - Avatar(firstName: contact.givenName, lastName: contact.familyName) + @ViewBuilder + private func contactsRows(data: [CNContact]) -> some View { + if !data.isEmpty { + HStack(spacing: .zero) { + Text("Contacts") + Spacer() + } + .title3() + .onSurfaceSecondaryForeground() + .padding(.vertical, .xxSmall) + .paddingContent(.horizontal) + .padding(.top, viewModel.lastSelectedEmails.isEmpty ? .zero : .small) + + ForEach(data, id: \.identifier) { contact in + let emails = contact.emailAddresses + if !emails.isEmpty { + ForEach(emails, id: \.identifier) { email in + emailRow(email: email, contact: contact) } - - }) - #endif + } + } } + } - private func onDoneAction() { - if viewModel.searchText.isEmail { - if !selection.contains(viewModel.searchText) { - selection.append(viewModel.searchText) + @ViewBuilder + private func emailRow(email: CNLabeledValue, contact: CNContact) -> some View { + let email = email.value as String + let isSelected = selectedEmails.contains(email) + #if os(iOS) + if let avatarThumbnailData = contact.thumbnailImageData, let avatarThumbnail = UIImage(data: avatarThumbnailData) { + Checkbox(isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), label: { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName, avatar: Image(uiImage: avatarThumbnail)) } - if !viewModel.lastSelectedEmails.contains(viewModel.searchText) { - viewModel.lastSelectedEmails.append(viewModel.searchText) + }) + } else { + Checkbox(isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), label: { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName) } + + }) + } + #else + Checkbox(isOn: Binding( + get: { isSelected }, + set: { _ in onContactClick(email: email) } + ), label: { + Row(contact.givenName + " " + contact.familyName, subtitle: email) { + Avatar(firstName: contact.givenName, lastName: contact.familyName) } - if !selectedEmails.isEmpty { - for email in selectedEmails where !selection.contains(email) { - selection.append(email) - } + }) + #endif + } - for email in selectedEmails where !viewModel.lastSelectedEmails.contains(email) { - viewModel.lastSelectedEmails.append(email) - } + private func onDoneAction() { + if viewModel.searchText.isEmail { + if !selection.contains(viewModel.searchText) { + selection.append(viewModel.searchText) } - dismiss() + if !viewModel.lastSelectedEmails.contains(viewModel.searchText) { + viewModel.lastSelectedEmails.append(viewModel.searchText) + } } - private func onContactClick(email: String) { - let isSelected = selectedEmails.contains(email) - if isSelected { - selectedEmails.remove(email) - } else { - selectedEmails.append(email) + if !selectedEmails.isEmpty { + for email in selectedEmails where !selection.contains(email) { + selection.append(email) + } + + for email in selectedEmails where !viewModel.lastSelectedEmails.contains(email) { + viewModel.lastSelectedEmails.append(email) } } - @ViewBuilder - private func placeholder() -> some View { - #if os(watchOS) - ProgressView() - #else - LoaderOverlayView() - #endif + dismiss() + } + + private func onContactClick(email: String) { + let isSelected = selectedEmails.contains(email) + if isSelected { + selectedEmails.remove(email) + } else { + selectedEmails.append(email) } } + + @ViewBuilder + private func placeholder() -> some View { + #if os(watchOS) + ProgressView() + #else + LoaderOverlayView() + #endif + } +} #endif // struct ContactsPickerView_Previews: PreviewProvider { // static var previews: some View { diff --git a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift index 243dca3..2cdd47d 100644 --- a/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift +++ b/Sources/OversizeContactsKit/ContactsPicker/EmailPickerViewModel.swift @@ -4,7 +4,7 @@ // #if canImport(Contacts) - @preconcurrency import Contacts +@preconcurrency import Contacts #endif import Factory import OversizeContactsService @@ -13,53 +13,53 @@ import OversizeModels import SwiftUI #if !os(tvOS) - @MainActor - class EmailPickerViewModel: ObservableObject { - @Injected(\.contactsService) private var contactsService: ContactsService - @Published var state = ContactsPickerViewModelState.initial - @Published var searchText: String = .init() +@MainActor +class EmailPickerViewModel: ObservableObject { + @Injected(\.contactsService) private var contactsService: ContactsService + @Published var state = ContactsPickerViewModelState.initial + @Published var searchText: String = .init() - @AppStorage("AppState.LastSelectedEmails") var lastSelectedEmails: [String] = .init() + @AppStorage("AppState.LastSelectedEmails") var lastSelectedEmails: [String] = .init() - func fetchData() async { - state = .loading - let status = await contactsService.requestAccess() - switch status { - case .success: - - let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] - let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) - switch result { - case let .success(data): - log("✅ CNContact fetched") - state = .result(data) - case let .failure(error): - log("❌ CNContact not fetched (\(error.title))") - state = .error(error) - } + func fetchData() async { + state = .loading + let status = await contactsService.requestAccess() + switch status { + case .success: + let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactThumbnailImageDataKey] + let result = await contactsService.fetchContacts(keysToFetch: keys as [CNKeyDescriptor]) + switch result { + case let .success(data): + log("✅ CNContact fetched") + state = .result(data) case let .failure(error): + log("❌ CNContact not fetched (\(error.title))") state = .error(error) } + + case let .failure(error): + state = .error(error) } + } - func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { - for contact in contacts where !contact.emailAddresses.isEmpty { - for emailAddress in contact.emailAddresses { - let emailAddressString = emailAddress.value as String - if emailAddressString == email { - return contact - } + func getContactFromEmail(email: String, contacts: [CNContact]) -> CNContact? { + for contact in contacts where !contact.emailAddresses.isEmpty { + for emailAddress in contact.emailAddresses { + let emailAddressString = emailAddress.value as String + if emailAddressString == email { + return contact } } - return nil } + return nil } +} - enum ContactsPickerViewModelState { - case initial - case loading - case result([CNContact]) - case error(AppError) - } +enum ContactsPickerViewModelState { + case initial + case loading + case result([CNContact]) + case error(AppError) +} #endif diff --git a/Sources/OversizeKit/AdsKit/AdView.swift b/Sources/OversizeKit/AdsKit/AdView.swift index b2e4836..df6acf4 100644 --- a/Sources/OversizeKit/AdsKit/AdView.swift +++ b/Sources/OversizeKit/AdsKit/AdView.swift @@ -33,16 +33,16 @@ public struct AdView: View { case let .result(appAd): #if os(iOS) - Surface { - isShowProduct.toggle() - } label: { - premiumBanner(appAd: appAd) - } - .surfaceContentMargins(.xSmall) - .appStoreOverlay(isPresent: $isShowProduct, appId: appAd.appStoreId) + Surface { + isShowProduct.toggle() + } label: { + premiumBanner(appAd: appAd) + } + .surfaceContentMargins(.xSmall) + .appStoreOverlay(isPresent: $isShowProduct, appId: appAd.appStoreId) #else - EmptyView() + EmptyView() #endif case .loading, .error: @@ -57,13 +57,17 @@ public struct AdView: View { $0 .resizable() .frame(width: 64, height: 64) - .mask(RoundedRectangle(cornerRadius: .large, - style: .continuous)) + .mask(RoundedRectangle( + cornerRadius: .large, + style: .continuous + )) .overlay( - RoundedRectangle(cornerRadius: 16, - style: .continuous) - .stroke(lineWidth: 1) - .opacity(0.15) + RoundedRectangle( + cornerRadius: 16, + style: .continuous + ) + .stroke(lineWidth: 1) + .opacity(0.15) ) .onTapGesture { isShowProduct.toggle() diff --git a/Sources/OversizeKit/DeeplinkKit/DeeplinkModifier.swift b/Sources/OversizeKit/DeeplinkKit/DeeplinkModifier.swift new file mode 100644 index 0000000..a259629 --- /dev/null +++ b/Sources/OversizeKit/DeeplinkKit/DeeplinkModifier.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2024 Alexander Romanov +// DeeplinkModifier.swift, created on 12.11.2024 +// + +import SwiftUI + +public struct DeeplinkModifier: ViewModifier { + private let pub = NotificationCenter.default.publisher(for: NSNotification.Name("Deeplink")) + private var onReceive: (URL) -> Void + + public init(onReceive: @escaping (URL) -> Void) { + self.onReceive = onReceive + } + + public func body(content: Content) -> some View { + content + .onReceive(pub) { output in + if let userInfo = output.userInfo, let info = userInfo["link"] as? String, let url = URL(string: info) { + onReceive(url) + } + } + } +} + +public extension View { + func onDeeplink(perform action: @escaping (URL) -> Void) -> some View { + modifier(DeeplinkModifier(onReceive: action)) + } +} diff --git a/Sources/OversizeKit/LauncherKit/Launcher.swift b/Sources/OversizeKit/LauncherKit/Launcher.swift index 51a704f..88f9a97 100644 --- a/Sources/OversizeKit/LauncherKit/Launcher.swift +++ b/Sources/OversizeKit/LauncherKit/Launcher.swift @@ -39,7 +39,7 @@ public struct Launcher: View { .systemServices() #if os(macOS) .frame(width: 840, height: 672) - // .interactiveDismissDisabled(!viewModel.appStateService.isCompletedOnbarding) + // .interactiveDismissDisabled(!viewModel.appStateService.isCompletedOnbarding) #endif } .onChange(of: viewModel.appStateService.isCompletedOnbarding) { _, isCompletedOnbarding in @@ -142,9 +142,9 @@ private extension View { item: Binding, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Item) -> some View ) -> some View where Item: Identifiable { #if os(macOS) - sheet(item: item, onDismiss: onDismiss, content: content) + sheet(item: item, onDismiss: onDismiss, content: content) #else - fullScreenCover(item: item, onDismiss: onDismiss, content: content) + fullScreenCover(item: item, onDismiss: onDismiss, content: content) #endif } } diff --git a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift index 8899600..d3305d2 100644 --- a/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift +++ b/Sources/OversizeKit/LauncherKit/LauncherViewModel.swift @@ -10,7 +10,7 @@ import OversizeStoreService import OversizeUI import SwiftUI #if canImport(LocalAuthentication) - import LocalAuthentication +import LocalAuthentication #endif import Factory @@ -36,12 +36,12 @@ public final class LauncherViewModel: ObservableObject { var isShowLockscreen: Bool { if FeatureFlags.secure.lookscreen ?? false { if settingsService.pinCodeEnabend || settingsService.biometricEnabled, authState != .unlocked { - return true + true } else { - return false + false } } else { - return false + false } } @@ -56,10 +56,10 @@ extension LauncherViewModel { case specialOffer(event: Components.Schemas.SpecialOffer) public var id: Int { switch self { - case .onboarding: return 0 - case .payWall: return 1 - case .rate: return 2 - case .specialOffer: return 3 + case .onboarding: 0 + case .payWall: 1 + case .rate: 2 + case .specialOffer: 3 } } } @@ -152,9 +152,9 @@ public extension LauncherViewModel { func checkDateInSelectedPeriod(startDate: Date, endDate: Date) -> Bool { if startDate < endDate { - return (startDate ... endDate).contains(Date()) + (startDate ... endDate).contains(Date()) } else { - return false + false } } diff --git a/Sources/OversizeKit/LauncherKit/SplashScreen.swift b/Sources/OversizeKit/LauncherKit/SplashScreen.swift index 8b70845..e098184 100644 --- a/Sources/OversizeKit/LauncherKit/SplashScreen.swift +++ b/Sources/OversizeKit/LauncherKit/SplashScreen.swift @@ -13,14 +13,16 @@ struct SplashScreen: View { Color.accent #if os(iOS) - if let appImage = Info.app.iconName { - Image(uiImage: UIImage(named: appImage) ?? UIImage()) - .resizable() - .frame(width: 128, height: 128) - .mask(RoundedRectangle(cornerRadius: 28, - style: .continuous)) - .padding(.top, Space.xxLarge) - } + if let appImage = Info.app.iconName { + Image(uiImage: UIImage(named: appImage) ?? UIImage()) + .resizable() + .frame(width: 128, height: 128) + .mask(RoundedRectangle( + cornerRadius: 28, + style: .continuous + )) + .padding(.top, Space.xxLarge) + } #endif } diff --git a/Sources/OversizeKit/LockscreenKit/LockscreenView.swift b/Sources/OversizeKit/LockscreenKit/LockscreenView.swift index cfb01ef..03ede68 100644 --- a/Sources/OversizeKit/LockscreenKit/LockscreenView.swift +++ b/Sources/OversizeKit/LockscreenKit/LockscreenView.swift @@ -14,8 +14,8 @@ public enum LockscreenViewState { public struct LockscreenView: View { #if os(iOS) - @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? - @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? #endif @Environment(\.scenePhase) var scenePhase: ScenePhase @@ -47,31 +47,32 @@ public struct LockscreenView: View { private var isShowTitle: Bool { #if os(iOS) - if horizontalSizeClass == .compact, verticalSizeClass == .regular { - return true - } else if horizontalSizeClass == .regular, verticalSizeClass == .compact { - return false - } else if horizontalSizeClass == .regular, verticalSizeClass == .regular { - return true - } else { - return true - } - #else + if horizontalSizeClass == .compact, verticalSizeClass == .regular { + return true + } else if horizontalSizeClass == .regular, verticalSizeClass == .compact { + return false + } else if horizontalSizeClass == .regular, verticalSizeClass == .regular { return true + } else { + return true + } + #else + return true #endif } - public init(pinCode: Binding, - state: Binding = .constant(.locked), - maxCount: Int = 4, - title: String? = nil, - errorText: String? = nil, - pinCodeEnabled: Bool = true, - biometricEnabled: Bool = false, - biometricType: BiometricType = .faceID, - action: (() -> Void)? = nil, - biometricAction: (() -> Void)? = nil) - { + public init( + pinCode: Binding, + state: Binding = .constant(.locked), + maxCount: Int = 4, + title: String? = nil, + errorText: String? = nil, + pinCodeEnabled: Bool = true, + biometricEnabled: Bool = false, + biometricType: BiometricType = .faceID, + action: (() -> Void)? = nil, + biometricAction: (() -> Void)? = nil + ) { _pinCode = pinCode _state = state self.maxCount = maxCount @@ -116,16 +117,18 @@ public struct LockscreenView: View { if let appImage = Info.app.iconName { #if os(iOS) - Image(uiImage: UIImage(named: appImage) ?? UIImage()) - .resizable() - .frame(width: 96, height: 96) - .mask(RoundedRectangle(cornerRadius: 26, - style: .continuous)) + Image(uiImage: UIImage(named: appImage) ?? UIImage()) + .resizable() + .frame(width: 96, height: 96) + .mask(RoundedRectangle( + cornerRadius: 26, + style: .continuous + )) #else - Text(biometricType.rawValue) - .title2(.bold) - .foregroundColor(.onSurfacePrimary) + Text(biometricType.rawValue) + .title2(.bold) + .foregroundColor(.onSurfacePrimary) #endif @@ -139,18 +142,18 @@ public struct LockscreenView: View { #if os(iOS) - Button { biometricAction?() } label: { - HStack(spacing: .xSmall) { - biometricImage() - .padding(.leading, 2) + Button { biometricAction?() } label: { + HStack(spacing: .xSmall) { + biometricImage() + .padding(.leading, 2) - Text("Open with \(biometricType.rawValue)") - } - .padding(.horizontal, .xxxSmall) + Text("Open with \(biometricType.rawValue)") } - .buttonStyle(.tertiary(infinityWidth: false)) - .controlBorderShape(.capsule) - .controlSize(.small) + .padding(.horizontal, .xxxSmall) + } + .buttonStyle(.tertiary(infinityWidth: false)) + .controlBorderShape(.capsule) + .controlSize(.small) #endif Spacer() @@ -285,9 +288,12 @@ public struct LockscreenView: View { .offset(x: leftOffset) // .animation(Animation.easeInOut(duration: 1).delay(0.2 * Double(number))) .scaleEffect(shouldAnimate ? 0.5 : 1) - .animation(Animation.easeInOut(duration: 0.5) - .repeatForever() - .delay(number == 0 ? 0 : 0.5 * Double(number)), value: shouldAnimate) + .animation( + Animation.easeInOut(duration: 0.5) + .repeatForever() + .delay(number == 0 ? 0 : 0.5 * Double(number)), + value: shouldAnimate + ) } } .onReceive(timer) { _ in diff --git a/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift b/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift index 0da5cbe..a48ca82 100644 --- a/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift +++ b/Sources/OversizeKit/SettingsKit/SettingsRouter/ResolveRouter.swift @@ -52,13 +52,13 @@ extension SettingsScreen: @preconcurrency RoutableView { WebView(url: url) case let .sendMail(to: to, subject: subject, content: content): #if os(iOS) - MailView( - to: to, - subject: subject, - content: content - ) + MailView( + to: to, + subject: subject, + content: content + ) #else - EmptyView() + EmptyView() #endif } } diff --git a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift index d852126..bcfffa2 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/About/AboutView.swift @@ -15,7 +15,7 @@ import SwiftUI // swiftlint:disable all #if canImport(MessageUI) - import MessageUI +import MessageUI #endif public struct AboutView: View { @@ -42,42 +42,42 @@ public struct AboutView: View { var oppacity: CGFloat { if offset < 0 { - return 1 + 1 } else if offset > 500 { - return 0 + 0 } else { - return Double(1 / (offset * 0.01)) + Double(1 / (offset * 0.01)) } } var blur: CGFloat { if offset < 0 { - return 0 + 0 } else { - return Double(offset * 0.05) + Double(offset * 0.05) } } #if os(iOS) - let scale = UIScreen.main.scale + let scale = UIScreen.main.scale #else - let scale: CGFloat = 2 + let scale: CGFloat = 2 #endif public var body: some View { #if os(iOS) - Page(L10n.Settings.about) { - list - .surfaceContentRowMargins() - .task { - await viewModel.fetchApps() - } - } - .backgroundSecondary() + Page(L10n.Settings.about) { + list + .surfaceContentRowMargins() + .task { + await viewModel.fetchApps() + } + } + .backgroundSecondary() #else - list - .navigationTitle(L10n.Settings.about) + list + .navigationTitle(L10n.Settings.about) #endif } @@ -101,13 +101,17 @@ public struct AboutView: View { $0 .resizable() .frame(width: 74, height: 74) - .mask(RoundedRectangle(cornerRadius: .large, - style: .continuous)) + .mask(RoundedRectangle( + cornerRadius: .large, + style: .continuous + )) .overlay( - RoundedRectangle(cornerRadius: 16, - style: .continuous) - .stroke(lineWidth: 1) - .opacity(0.15) + RoundedRectangle( + cornerRadius: 16, + style: .continuous + ) + .stroke(lineWidth: 1) + .opacity(0.15) ) }, placeholder: { @@ -215,42 +219,42 @@ public struct AboutView: View { } #if os(iOS) - if MFMailComposeViewController.canSendMail(), - let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, - let appName = Info.app.name, - let device = Info.app.device, - let appBuild = Info.app.build, - let systemVersion = Info.app.system - { - let contentPreText = "\n\n\n\n\n\n————————————————\nApp: \(appName) \(appVersion) (\(appBuild))\nDevice: \(device), \(systemVersion)\nLocale: \(Info.app.language ?? "Not init")" - let subject = "Feedback" - - Row(L10n.About.suggestIdea) { - isShowMail.toggle() - } leading: { - ideaSettingsIcon.icon() - } + if MFMailComposeViewController.canSendMail(), + let mail = Info.links?.company.email, + let appVersion = Info.app.verstion, + let appName = Info.app.name, + let device = Info.app.device, + let appBuild = Info.app.build, + let systemVersion = Info.app.system + { + let contentPreText = "\n\n\n\n\n\n————————————————\nApp: \(appName) \(appVersion) (\(appBuild))\nDevice: \(device), \(systemVersion)\nLocale: \(Info.app.language ?? "Not init")" + let subject = "Feedback" + + Row(L10n.About.suggestIdea) { + isShowMail.toggle() + } leading: { + ideaSettingsIcon.icon() + } - .buttonStyle(.row) - .sheet(isPresented: $isShowMail) { - MailView(to: mail, subject: subject, content: contentPreText) - } + .buttonStyle(.row) + .sheet(isPresented: $isShowMail) { + MailView(to: mail, subject: subject, content: contentPreText) } + } #endif #if os(iOS) - if let shareUrl = Info.url.appInstallShare, let id = Info.app.appStoreID, !id.isEmpty { - Row(L10n.Settings.shareApplication) { - isSharePresented.toggle() - } leading: { - shareSettingsIcon.icon() - } - .sheet(isPresented: $isSharePresented) { - ActivityViewController(activityItems: [shareUrl]) - .presentationDetents([.medium, .large]) - } + if let shareUrl = Info.url.appInstallShare, let id = Info.app.appStoreID, !id.isEmpty { + Row(L10n.Settings.shareApplication) { + isSharePresented.toggle() + } leading: { + shareSettingsIcon.icon() } + .sheet(isPresented: $isSharePresented) { + ActivityViewController(activityItems: [shareUrl]) + .presentationDetents([.medium, .large]) + } + } #endif } } @@ -496,33 +500,33 @@ public struct AboutView: View { var rateSettingsIcon: Image { switch iconStyle { case .line: - return Image.Base.heart + Image.Base.heart case .fill: - return Image.Base.Heart.fill + Image.Base.Heart.fill case .twoTone: - return Image.Base.Heart.TwoTone.fill + Image.Base.Heart.TwoTone.fill } } var ideaSettingsIcon: Image { switch iconStyle { case .line: - return Image.Electricity.lamp + Image.Electricity.lamp case .fill: - return Image.Electricity.Lamp.fill + Image.Electricity.Lamp.fill case .twoTone: - return Image.Electricity.Lamp.TwoTone.fill + Image.Electricity.Lamp.TwoTone.fill } } var shareSettingsIcon: Image { switch iconStyle { case .line: - return Image.Base.send + Image.Base.send case .fill: - return Image.Base.Send.fill + Image.Base.Send.fill case .twoTone: - return Image.Base.Send.TwoTone.fill + Image.Base.Send.TwoTone.fill } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift index bd8491f..c722787 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/FeedbackView.swift @@ -4,7 +4,7 @@ // #if canImport(MessageUI) - import MessageUI +import MessageUI #endif import OversizeComponents import OversizeLocalizable @@ -58,33 +58,33 @@ public struct FeedbackView: View { VStack(alignment: .leading) { #if os(iOS) - if MFMailComposeViewController.canSendMail(), - let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, - let appName = Info.app.name, - let device = Info.app.device, - let appBuild = Info.app.build, - let systemVersion = Info.app.system - { - let contentPreText = "\n\n\n\n\n\n————————————————\nApp: \(appName) \(appVersion) (\(appBuild))\nDevice: \(device), \(systemVersion)\nLocale: \(Info.app.language ?? "Not init")" - let subject = "Feedback" + if MFMailComposeViewController.canSendMail(), + let mail = Info.links?.company.email, + let appVersion = Info.app.verstion, + let appName = Info.app.name, + let device = Info.app.device, + let appBuild = Info.app.build, + let systemVersion = Info.app.system + { + let contentPreText = "\n\n\n\n\n\n————————————————\nApp: \(appName) \(appVersion) (\(appBuild))\nDevice: \(device), \(systemVersion)\nLocale: \(Info.app.language ?? "Not init")" + let subject = "Feedback" - Row(L10n.Settings.feedbakAuthor) { - router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) - } leading: { - mailIcon.icon() - } - } else { - // Send author - if let sendMailUrl = Info.url.developerSendMail { - Link(destination: sendMailUrl) { - Row(L10n.Settings.feedbakAuthor) { - mailIcon.icon() - } + Row(L10n.Settings.feedbakAuthor) { + router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) + } leading: { + mailIcon.icon() + } + } else { + // Send author + if let sendMailUrl = Info.url.developerSendMail { + Link(destination: sendMailUrl) { + Row(L10n.Settings.feedbakAuthor) { + mailIcon.icon() } - .buttonStyle(.row) } + .buttonStyle(.row) } + } #endif // Telegramm chat @@ -104,33 +104,33 @@ public struct FeedbackView: View { var heartIcon: Image { switch iconStyle { case .line: - return Image.Brands.appStore + Image.Brands.appStore case .fill: - return Image.Brands.AppStore.fill + Image.Brands.AppStore.fill case .twoTone: - return Image.Brands.AppStore.twoTone + Image.Brands.AppStore.twoTone } } var mailIcon: Image { switch iconStyle { case .line: - return Image.Email.email + Image.Email.email case .fill: - return Image.Email.Email.fill + Image.Email.Email.fill case .twoTone: - return Image.Email.Email.twoTone + Image.Email.Email.twoTone } } var chatIcon: Image { switch iconStyle { case .line: - return Image.Brands.telegram + Image.Brands.telegram case .fill: - return Image.Brands.Telegram.fill + Image.Brands.Telegram.fill case .twoTone: - return Image.Brands.Telegram.twoTone + Image.Brands.Telegram.twoTone } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift b/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift index ee5cae7..991ca77 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/OurResorsesView.swift @@ -4,7 +4,7 @@ // #if canImport(MessageUI) - import MessageUI +import MessageUI #endif import OversizeComponents import OversizeLocalizable @@ -53,22 +53,22 @@ public struct OurResorsesView: View { var figmaIcon: Image { switch iconStyle { case .line: - return Image.Brands.figma + Image.Brands.figma case .fill: - return Image.Brands.figma + Image.Brands.figma case .twoTone: - return Image.Brands.figma + Image.Brands.figma } } var githubIcon: Image { switch iconStyle { case .line: - return Image.Brands.github + Image.Brands.github case .fill: - return Image.Brands.github + Image.Brands.github case .twoTone: - return Image.Brands.github + Image.Brands.github } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift index d0aa8cc..441e1e2 100644 --- a/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/About/SupportView.swift @@ -4,7 +4,7 @@ // #if canImport(MessageUI) - import MessageUI +import MessageUI #endif import OversizeComponents import OversizeLocalizable @@ -49,33 +49,33 @@ public struct SupportView: View { SectionView { VStack(alignment: .leading) { #if os(iOS) - if MFMailComposeViewController.canSendMail(), - let mail = Info.links?.company.email, - let appVersion = Info.app.verstion, - let appName = Info.app.name, - let device = Info.app.device, - let appBuild = Info.app.build, - let systemVersion = Info.app.system - { - let contentPreText = "\n\n\n\n\n\n————————————————\nApp: \(appName) \(appVersion) (\(appBuild))\nDevice: \(device), \(systemVersion)\nLocale: \(Info.app.language ?? "Not init")" - let subject = "Support" + if MFMailComposeViewController.canSendMail(), + let mail = Info.links?.company.email, + let appVersion = Info.app.verstion, + let appName = Info.app.name, + let device = Info.app.device, + let appBuild = Info.app.build, + let systemVersion = Info.app.system + { + let contentPreText = "\n\n\n\n\n\n————————————————\nApp: \(appName) \(appVersion) (\(appBuild))\nDevice: \(device), \(systemVersion)\nLocale: \(Info.app.language ?? "Not init")" + let subject = "Support" - Row("Contact Us") { - router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) - } leading: { - mailIcon.icon() - } - } else { - // Send author - if let sendMailUrl = Info.url.developerSendMail { - Link(destination: sendMailUrl) { - Row("Contact Us") { - mailIcon.icon() - } + Row("Contact Us") { + router.present(.sendMail(to: mail, subject: subject, content: contentPreText)) + } leading: { + mailIcon.icon() + } + } else { + // Send author + if let sendMailUrl = Info.url.developerSendMail { + Link(destination: sendMailUrl) { + Row("Contact Us") { + mailIcon.icon() } - .buttonStyle(.row) } + .buttonStyle(.row) } + } #endif // Telegramm chat @@ -95,33 +95,33 @@ public struct SupportView: View { var heartIcon: Image { switch iconStyle { case .line: - return Image.Brands.appStore + Image.Brands.appStore case .fill: - return Image.Brands.AppStore.fill + Image.Brands.AppStore.fill case .twoTone: - return Image.Brands.AppStore.twoTone + Image.Brands.AppStore.twoTone } } var mailIcon: Image { switch iconStyle { case .line: - return Image.Email.email + Image.Email.email case .fill: - return Image.Email.Email.fill + Image.Email.Email.fill case .twoTone: - return Image.Email.Email.twoTone + Image.Email.Email.twoTone } } var chatIcon: Image { switch iconStyle { case .line: - return Image.Brands.telegram + Image.Brands.telegram case .fill: - return Image.Brands.Telegram.fill + Image.Brands.Telegram.fill case .twoTone: - return Image.Brands.Telegram.twoTone + Image.Brands.Telegram.twoTone } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift index 89946c5..2747f82 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/AppearanceSettingView.swift @@ -17,7 +17,7 @@ public struct AppearanceSettingView: View { @Environment(\.isPremium) var isPremium: Bool #if os(iOS) - @StateObject var iconSettings = AppIconSettings() + @StateObject var iconSettings = AppIconSettings() #endif private let columns = [ @@ -28,33 +28,33 @@ public struct AppearanceSettingView: View { public var body: some View { #if os(iOS) - Page(L10n.Settings.apperance) { - iOSSettings - .surfaceContentRowMargins() - } - .backgroundSecondary() + Page(L10n.Settings.apperance) { + iOSSettings + .surfaceContentRowMargins() + } + .backgroundSecondary() #else - macSettings + macSettings #endif } #if os(iOS) - private var iOSSettings: some View { - LazyVStack(alignment: .leading, spacing: 0) { - apperance + private var iOSSettings: some View { + LazyVStack(alignment: .leading, spacing: 0) { + apperance - accentColor + accentColor - advanded + advanded - if iconSettings.iconNames.count > 1 { - appIcon - } + if iconSettings.iconNames.count > 1 { + appIcon } - .preferredColorScheme(theme.appearance.colorScheme) - .accentColor(theme.accentColor) } + .preferredColorScheme(theme.appearance.colorScheme) + .accentColor(theme.accentColor) + } #endif private var macSettings: some View { @@ -101,57 +101,59 @@ public struct AppearanceSettingView: View { } #if os(iOS) - private var accentColor: some View { - SectionView("Accent color") { - ColorSelector(selection: theme.$accentColor) - } + private var accentColor: some View { + SectionView("Accent color") { + ColorSelector(selection: theme.$accentColor) } + } #endif #if os(iOS) - private var appIcon: some View { - SectionView("App icon") { - LazyVGrid(columns: columns, spacing: 24) { - ForEach(0 ..< iconSettings.iconNames.count, id: \.self) { index in - HStack { - Image(uiImage: UIImage(named: iconSettings.iconNames[index] - ?? "AppIcon") ?? UIImage()) - .renderingMode(.original) - .resizable() - .scaledToFit() - .frame(width: 78, height: 78) - .cornerRadius(18) - .overlay( - RoundedRectangle(cornerRadius: 20) - .stroke(index == iconSettings.currentIndex ? Color.accent : Color.border, - lineWidth: index == iconSettings.currentIndex ? 3 : 1) - ) - .onTapGesture { - if index != 0, isPremium == false { - router.present(.premium) - } else { - let defaultIconIndex = iconSettings.iconNames - .firstIndex(of: UIApplication.shared.alternateIconName) ?? 0 - if defaultIconIndex != index { - // swiftlint:disable line_length - UIApplication.shared.setAlternateIconName(iconSettings.iconNames[index]) { error in - if let error { - log(error.localizedDescription) - } else { - log("Success! You have changed the app icon.") - } + private var appIcon: some View { + SectionView("App icon") { + LazyVGrid(columns: columns, spacing: 24) { + ForEach(0 ..< iconSettings.iconNames.count, id: \.self) { index in + HStack { + Image(uiImage: UIImage(named: iconSettings.iconNames[index] + ?? "AppIcon") ?? UIImage()) + .renderingMode(.original) + .resizable() + .scaledToFit() + .frame(width: 78, height: 78) + .cornerRadius(18) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke( + index == iconSettings.currentIndex ? Color.accent : Color.border, + lineWidth: index == iconSettings.currentIndex ? 3 : 1 + ) + ) + .onTapGesture { + if index != 0, isPremium == false { + router.present(.premium) + } else { + let defaultIconIndex = iconSettings.iconNames + .firstIndex(of: UIApplication.shared.alternateIconName) ?? 0 + if defaultIconIndex != index { + // swiftlint:disable line_length + UIApplication.shared.setAlternateIconName(iconSettings.iconNames[index]) { error in + if let error { + log(error.localizedDescription) + } else { + log("Success! You have changed the app icon.") } } } } - } - .padding(3) + } } + .padding(3) } - .padding() } + .padding() } + } #endif private var advanded: some View { @@ -197,59 +199,59 @@ public struct AppearanceSettingView: View { var textIcon: Image { switch iconStyle { case .line: - return Image.Editor.Font.square + Image.Editor.Font.square case .fill: - return Image.Editor.Font.Square.fill + Image.Editor.Font.Square.fill case .twoTone: - return Image.Editor.Font.Square.TwoTone.fill + Image.Editor.Font.Square.TwoTone.fill } } var borderIcon: Image { switch iconStyle { case .line: - return Image.Design.verticalMirror + Image.Design.verticalMirror case .fill: - return Image.Editor.Font.Square.fill + Image.Editor.Font.Square.fill case .twoTone: - return Image.Editor.Font.Square.TwoTone.fill + Image.Editor.Font.Square.TwoTone.fill } } var radiusIcon: Image { switch iconStyle { case .line: - return Image.Design.path + Image.Design.path case .fill: - return Image.Design.Path.fill + Image.Design.Path.fill case .twoTone: - return Image.Design.Path.twoTone + Image.Design.Path.twoTone } } } #if os(iOS) - @MainActor - public class AppIconSettings: ObservableObject { - public var iconNames: [String?] = [nil] - @Published public var currentIndex = 0 +@MainActor +public class AppIconSettings: ObservableObject { + public var iconNames: [String?] = [nil] + @Published public var currentIndex = 0 - public init() { - getAlternateIconNames() + public init() { + getAlternateIconNames() - if let currentIcon = UIApplication.shared.alternateIconName { - currentIndex = iconNames.firstIndex(of: currentIcon) ?? 0 - } + if let currentIcon = UIApplication.shared.alternateIconName { + currentIndex = iconNames.firstIndex(of: currentIcon) ?? 0 } + } - private func getAlternateIconNames() { - if let iconCount = FeatureFlags.app.alternateAppIcons, iconCount != 0 { - for index in 1 ... iconCount { - iconNames.append("AlternateAppIcon\(index)") - } + private func getAlternateIconNames() { + if let iconCount = FeatureFlags.app.alternateAppIcons, iconCount != 0 { + for index in 1 ... iconCount { + iconNames.append("AlternateAppIcon\(index)") } } } +} #endif struct SettingsThemeView_Previews: PreviewProvider { diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift index 6a41ab4..3a16be0 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/BorderSettingView.swift @@ -45,27 +45,27 @@ public struct BorderSettingView: View { if theme.borderApp { VStack(spacing: Space.small.rawValue) { #if os(iOS) || os(macOS) - Surface { - VStack(spacing: Space.xxSmall.rawValue) { - HStack { - Text("Size") - .subheadline() - .foregroundColor(.onSurfacePrimary) - - Spacer() - - Text(String(format: "%.1f", theme.borderSize) + " px") - .subheadline() - .foregroundColor(.onSurfacePrimary) - } - - Slider(value: theme.$borderSize, in: 0.5 ... 2, step: 0.5) + Surface { + VStack(spacing: Space.xxSmall.rawValue) { + HStack { + Text("Size") + .subheadline() + .foregroundColor(.onSurfacePrimary) + + Spacer() + + Text(String(format: "%.1f", theme.borderSize) + " px") + .subheadline() + .foregroundColor(.onSurfacePrimary) } + + Slider(value: theme.$borderSize, in: 0.5 ... 2, step: 0.5) } - .surfaceStyle(.secondary) - .surfaceContentMargins(.small) - .padding(.horizontal, Space.medium) - .padding(.bottom, Space.xxSmall) + } + .surfaceStyle(.secondary) + .surfaceContentMargins(.small) + .padding(.horizontal, Space.medium) + .padding(.bottom, Space.xxSmall) #endif diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift index ad9c8cf..8112526 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/FontSettingView.swift @@ -50,39 +50,45 @@ public struct FontSettingView: View { } private var titleSelector: some View { - GridSelect(FontDesignType.allCases, selection: theme.$fontTitle, - content: { fontStyle, _ in - HStack { - VStack(alignment: .leading, spacing: 8) { - Text("Aa") - .font(.system(size: 34, weight: .heavy, design: fontStyle.system)) - .foregroundColor(.onSurfacePrimary) - - Text(fontStyle.rawValue.capitalizingFirstLetter()) - .font(.system(size: 16, weight: .medium, design: fontStyle.system)) - .foregroundColor(.onSurfacePrimary) - } - Spacer() - }.padding() - }).gridSelectStyle(.default(selected: .graySurface)) + GridSelect( + FontDesignType.allCases, + selection: theme.$fontTitle, + content: { fontStyle, _ in + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("Aa") + .font(.system(size: 34, weight: .heavy, design: fontStyle.system)) + .foregroundColor(.onSurfacePrimary) + + Text(fontStyle.rawValue.capitalizingFirstLetter()) + .font(.system(size: 16, weight: .medium, design: fontStyle.system)) + .foregroundColor(.onSurfacePrimary) + } + Spacer() + }.padding() + } + ).gridSelectStyle(.default(selected: .graySurface)) } private var paragraphSelector: some View { - GridSelect(FontDesignType.allCases, selection: theme.$fontParagraph, - content: { fontStyle, _ in - HStack { - VStack(alignment: .leading, spacing: 8) { - Text("Aa") - .font(.system(size: 34, weight: .heavy, design: fontStyle.system)) - .foregroundColor(.onSurfacePrimary) - - Text(fontStyle.rawValue.capitalizingFirstLetter()) - .font(.system(size: 16, weight: .medium, design: fontStyle.system)) - .foregroundColor(.onSurfacePrimary) - } - Spacer() - }.padding() - }).gridSelectStyle(.default(selected: .graySurface)) + GridSelect( + FontDesignType.allCases, + selection: theme.$fontParagraph, + content: { fontStyle, _ in + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("Aa") + .font(.system(size: 34, weight: .heavy, design: fontStyle.system)) + .foregroundColor(.onSurfacePrimary) + + Text(fontStyle.rawValue.capitalizingFirstLetter()) + .font(.system(size: 16, weight: .medium, design: fontStyle.system)) + .foregroundColor(.onSurfacePrimary) + } + Spacer() + }.padding() + } + ).gridSelectStyle(.default(selected: .graySurface)) } private var otherSelector: some View { diff --git a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift b/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift index 1d63b74..e7129e7 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Apperance/RadiusSettingView.swift @@ -28,23 +28,23 @@ public struct RadiusSettingView: View { VStack(spacing: .zero) { VStack(spacing: Space.small.rawValue) { #if os(iOS) || os(macOS) - VStack(spacing: Space.xxSmall.rawValue) { - HStack { - Text("Size") - .subheadline() - .foregroundColor(.onSurfacePrimary) + VStack(spacing: Space.xxSmall.rawValue) { + HStack { + Text("Size") + .subheadline() + .foregroundColor(.onSurfacePrimary) - Spacer() + Spacer() - Text(String(format: "%.0f", theme.radius) + " px") - .subheadline() - .foregroundColor(.onSurfacePrimary) - } - - Slider(value: theme.$radius, in: 0 ... 12, step: 4) + Text(String(format: "%.0f", theme.radius) + " px") + .subheadline() + .foregroundColor(.onSurfacePrimary) } - .padding(.horizontal, Space.medium) - .padding(.bottom, Space.xxSmall) + + Slider(value: theme.$radius, in: 0 ... 12, step: 4) + } + .padding(.horizontal, Space.medium) + .padding(.bottom, Space.xxSmall) #endif } diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift index c0484ee..a2710de 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/PINCode/SetPINCodeView.swift @@ -35,32 +35,35 @@ public struct SetPINCodeView: View { private func stateView(state: SetPINCodeViewState) -> some View { switch state { case .oldPINField: - LockscreenView(pinCode: $viewModel.oldCodeField, - state: $viewModel.authState, - maxCount: viewModel.maxCount, - title: L10n.Security.oldPINCode, - errorText: viewModel.errorText) - { + LockscreenView( + pinCode: $viewModel.oldCodeField, + state: $viewModel.authState, + maxCount: viewModel.maxCount, + title: L10n.Security.oldPINCode, + errorText: viewModel.errorText + ) { viewModel.chekOldPINCode() } biometricAction: {} case .newPINField: - LockscreenView(pinCode: $viewModel.newPinCodeField, - state: $viewModel.authState, - maxCount: viewModel.maxCount, - title: L10n.Security.newPINCode, - errorText: viewModel.errorText) - { + LockscreenView( + pinCode: $viewModel.newPinCodeField, + state: $viewModel.authState, + maxCount: viewModel.maxCount, + title: L10n.Security.newPINCode, + errorText: viewModel.errorText + ) { viewModel.checkNewPINCode() } biometricAction: {} case .confirmNewPINField: - LockscreenView(pinCode: $viewModel.confirmNewCodeField, - state: $viewModel.authState, - maxCount: viewModel.maxCount, - title: L10n.Security.confirmPINCode, - errorText: viewModel.errorText) - { + LockscreenView( + pinCode: $viewModel.confirmNewCodeField, + state: $viewModel.authState, + maxCount: viewModel.maxCount, + title: L10n.Security.confirmPINCode, + errorText: viewModel.errorText + ) { Task { let result = await viewModel.checkConfirmNewPINCode() switch result { diff --git a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift index dc09593..6256ce7 100644 --- a/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/Security/SecuritySettingsView.swift @@ -146,13 +146,13 @@ extension SecuritySettingsView { private var biometricImageName: String { switch biometricService.biometricType { case .none: - return "" + "" case .touchID: - return "touchid" + "touchid" case .faceID: - return "faceid" + "faceid" case .opticID: - return "opticid" + "opticid" } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift index 22fd90d..e207769 100644 --- a/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/SettingsView.swift @@ -31,12 +31,12 @@ public struct SettingsView: View { public var body: some View { #if os(iOS) - Page(L10n.Settings.title) { - iOSSettings - }.backgroundSecondary() + Page(L10n.Settings.title) { + iOSSettings + }.backgroundSecondary() #else - macSettings + macSettings #endif } @@ -44,26 +44,26 @@ public struct SettingsView: View { // iOS Settings #if os(iOS) - extension SettingsView { - private var iOSSettings: some View { - VStack(alignment: .center, spacing: 0) { - if let stoteKit = FeatureFlags.app.storeKit { - if stoteKit { - SectionView { - PrmiumBannerRow() - } - .surfaceContentMargins(.zero) +extension SettingsView { + private var iOSSettings: some View { + VStack(alignment: .center, spacing: 0) { + if let stoteKit = FeatureFlags.app.storeKit { + if stoteKit { + SectionView { + PrmiumBannerRow() } + .surfaceContentMargins(.zero) } - Group { - app - help - about - } - .surfaceContentRowMargins() } + Group { + app + help + about + } + .surfaceContentRowMargins() } } +} #endif extension SettingsView { @@ -136,66 +136,66 @@ extension SettingsView { var apperanceSettingsIcon: Image { switch iconStyle { case .line: - return Image.Design.paintingPalette + Image.Design.paintingPalette case .fill: - return Image.Design.PaintingPalette.fill + Image.Design.PaintingPalette.fill case .twoTone: - return Image.Design.PaintingPalette.twoTone + Image.Design.PaintingPalette.twoTone } } var cloudKitIcon: Image { switch iconStyle { case .line: - return Image.Weather.cloud2 + Image.Weather.cloud2 case .fill: - return Image.Weather.Cloud.Square.fill + Image.Weather.Cloud.Square.fill case .twoTone: - return Image.Weather.Cloud.Square.twoTone + Image.Weather.Cloud.Square.twoTone } } var securityIcon: Image { switch iconStyle { case .line: - return Image.Base.lock + Image.Base.lock case .fill: - return Image.Base.Lock.fill + Image.Base.Lock.fill case .twoTone: - return Image.Base.Lock.TwoTone.fill + Image.Base.Lock.TwoTone.fill } } var soundIcon: Image { switch iconStyle { case .line: - return Image.Base.volumeUp + Image.Base.volumeUp case .fill: - return Image.Base.VolumeUp.fill + Image.Base.VolumeUp.fill case .twoTone: - return Image.Base.VolumeUp.TwoTone.fill + Image.Base.VolumeUp.TwoTone.fill } } var vibrationIcon: Image { switch iconStyle { case .line: - return Image.Mobile.vibration + Image.Mobile.vibration case .fill: - return Image.Mobile.Vibration.fill + Image.Mobile.Vibration.fill case .twoTone: - return Image.Mobile.Vibration.twoTone + Image.Mobile.Vibration.twoTone } } var notificationsIcon: Image { switch iconStyle { case .line: - return Image.Base.notification + Image.Base.notification case .fill: - return Image.Base.Notification.fill + Image.Base.Notification.fill case .twoTone: - return Image.Base.Notification.TwoTone.fill + Image.Base.Notification.TwoTone.fill } } @@ -205,7 +205,7 @@ extension SettingsView { VStack(alignment: .leading) { Row("Get help") { #if os(iOS) - router.present(.support, detents: [.medium]) + router.present(.support, detents: [.medium]) #endif } leading: { helpIcon.icon() @@ -215,7 +215,7 @@ extension SettingsView { Row("Send feedback") { #if os(iOS) - router.present(.feedback, detents: [.medium]) + router.present(.feedback, detents: [.medium]) #endif } leading: { chatIcon.icon() @@ -229,66 +229,66 @@ extension SettingsView { var heartIcon: Image { switch iconStyle { case .line: - return Image.Base.heart + Image.Base.heart case .fill: - return Image.Base.Heart.fill + Image.Base.Heart.fill case .twoTone: - return Image.Base.Heart.TwoTone.fill + Image.Base.Heart.TwoTone.fill } } var mailIcon: Image { switch iconStyle { case .line: - return Image.Base.message + Image.Base.message case .fill: - return Image.Base.Message.fill + Image.Base.Message.fill case .twoTone: - return Image.Base.Message.TwoTone.fill + Image.Base.Message.TwoTone.fill } } var chatIcon: Image { switch iconStyle { case .line: - return Image.Base.chat + Image.Base.chat case .fill: - return Image.Base.Chat.fill + Image.Base.Chat.fill case .twoTone: - return Image.Base.Chat.twoTone + Image.Base.Chat.twoTone } } var infoIcon: Image { switch iconStyle { case .line: - return Image.Base.Info.circle + Image.Base.Info.circle case .fill: - return Image.Base.Info.Circle.fill + Image.Base.Info.Circle.fill case .twoTone: - return Image.Base.Info.Circle.twoTone + Image.Base.Info.Circle.twoTone } } var oversizeIcon: Image { switch iconStyle { case .line: - return Image.Brands.oversize + Image.Brands.oversize case .fill: - return Image.Brands.Oversize.fill + Image.Brands.Oversize.fill case .twoTone: - return Image.Brands.Oversize.TwoTone.fill + Image.Brands.Oversize.TwoTone.fill } } var helpIcon: Image { switch iconStyle { case .line: - return Image.Alert.Help.circle + Image.Alert.Help.circle case .fill: - return Image.Alert.Help.Circle.fill + Image.Alert.Help.Circle.fill case .twoTone: - return Image.Alert.Help.Circle.twoTone + Image.Alert.Help.Circle.twoTone } } @@ -308,13 +308,13 @@ extension SettingsView { var soundsAndVibrationTitle: String { if FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.soundsAndVibration + L10n.Settings.soundsAndVibration } else if FeatureFlags.app.sounds.valueOrFalse, !FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.sounds + L10n.Settings.sounds } else if !FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.vibration + L10n.Settings.vibration } else { - return "" + "" } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift index 6465b3a..fa1549c 100644 --- a/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/SoundAndVibration/SoundsAndVibrationsSettingsView.swift @@ -25,13 +25,13 @@ public struct SoundsAndVibrationsSettingsView: View { private var title: String { if FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.soundsAndVibration + L10n.Settings.soundsAndVibration } else if FeatureFlags.app.sounds.valueOrFalse, !FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.sounds + L10n.Settings.sounds } else if !FeatureFlags.app.sounds.valueOrFalse, FeatureFlags.app.vibration.valueOrFalse { - return L10n.Settings.vibration + L10n.Settings.vibration } else { - return "" + "" } } } @@ -70,11 +70,11 @@ extension SoundsAndVibrationsSettingsView { var vibrationIcon: Image { switch iconStyle { case .line: - return Image.Mobile.vibration + Image.Mobile.vibration case .fill: - return Image.Mobile.Vibration.fill + Image.Mobile.Vibration.fill case .twoTone: - return Image.Mobile.Vibration.twoTone + Image.Mobile.Vibration.twoTone } } } diff --git a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift index 6a42b66..b8042c3 100644 --- a/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift +++ b/Sources/OversizeKit/SettingsKit/Views/iCloud/iCloudSettingsView.swift @@ -48,9 +48,10 @@ extension iCloudSettingsView { if FeatureFlags.secure.CVVCodes.valueOrFalse { Switch(isOn: $settingsService.cloudKitCVVEnabled) { - Row(L10n.Security.iCloudSyncCVVDescriptionCloudKit, - subtitle: settingsService.cloudKitCVVEnabled ? L10n.Security.iCloudSyncCVVDescriptionCloudKit : L10n.Security.iCloudSyncCVVDescriptionLocal) - { + Row( + L10n.Security.iCloudSyncCVVDescriptionCloudKit, + subtitle: settingsService.cloudKitCVVEnabled ? L10n.Security.iCloudSyncCVVDescriptionCloudKit : L10n.Security.iCloudSyncCVVDescriptionLocal + ) { Image.Security.cloudLock .icon() .frame(width: 24, height: 24) diff --git a/Sources/OversizeKit/StateKit/LoadingViewState.swift b/Sources/OversizeKit/StateKit/LoadingViewState.swift index 05176b8..4ae0315 100644 --- a/Sources/OversizeKit/StateKit/LoadingViewState.swift +++ b/Sources/OversizeKit/StateKit/LoadingViewState.swift @@ -17,33 +17,42 @@ public extension LoadingViewState { var isLoading: Bool { switch self { case .loading, .idle: - return true + true default: - return false + false } } var result: Result? { switch self { case let .result(result): - return result + result default: - return nil + nil + } + } + + var error: AppError? { + switch self { + case let .error(error): + error + default: + nil } } static func == (lhs: LoadingViewState, rhs: LoadingViewState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): - return true + true case (.loading, .loading): - return true + true case (.result, .result): - return true + true case (.error, .error): - return true + true default: - return false + false } } } diff --git a/Sources/OversizeKit/StateKit/ResultExtension.swift b/Sources/OversizeKit/StateKit/ResultExtension.swift new file mode 100644 index 0000000..e3423e7 --- /dev/null +++ b/Sources/OversizeKit/StateKit/ResultExtension.swift @@ -0,0 +1,34 @@ +// +// Copyright © 2024 Alexander Romanov +// File.swift, created on 27.11.2024 +// + +import Foundation +import OversizeModels + +public extension Result { + var failureError: Failure? { + switch self { + case let .failure(error): error + case .success: nil + } + } + + var successResult: Success? { + switch self { + case .failure: nil + case let .success(value): value + } + } +} + +public extension Result { + var isFailure: Bool { !isSuccess } + + var isSuccess: Bool { + switch self { + case .failure: false + case .success: true + } + } +} diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductView.swift index 96e6aed..ef6de0f 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductView.swift @@ -6,35 +6,35 @@ import SwiftUI #if os(iOS) - public struct AppStoreProductViewControllerRepresentable: UIViewControllerRepresentable { - public typealias UIViewControllerType = AppStoreProductViewController +public struct AppStoreProductViewControllerRepresentable: UIViewControllerRepresentable { + public typealias UIViewControllerType = AppStoreProductViewController - private var isPresentStoreProduct: Binding - private let appId: String + private var isPresentStoreProduct: Binding + private let appId: String - public init(isPresentStoreProduct: Binding, appId: String) { - self.isPresentStoreProduct = isPresentStoreProduct - self.appId = appId - } + public init(isPresentStoreProduct: Binding, appId: String) { + self.isPresentStoreProduct = isPresentStoreProduct + self.appId = appId + } - public func makeUIViewController(context _: Context) -> UIViewControllerType { - let viewController = AppStoreProductViewController(isPresentStoreProduct: isPresentStoreProduct, appId: appId) - return viewController - } + public func makeUIViewController(context _: Context) -> UIViewControllerType { + let viewController = AppStoreProductViewController(isPresentStoreProduct: isPresentStoreProduct, appId: appId) + return viewController + } - public func updateUIViewController(_ uiViewController: UIViewControllerType, context _: Context) { - if isPresentStoreProduct.wrappedValue { - uiViewController.presentStoreProduct() - } + public func updateUIViewController(_ uiViewController: UIViewControllerType, context _: Context) { + if isPresentStoreProduct.wrappedValue { + uiViewController.presentStoreProduct() } } +} - public extension View { - func appStoreOverlay(isPresent: Binding, appId: String) -> some View { - background { - AppStoreProductViewControllerRepresentable(isPresentStoreProduct: isPresent, appId: appId) - .frame(width: 0, height: 0) - } +public extension View { + func appStoreOverlay(isPresent: Binding, appId: String) -> some View { + background { + AppStoreProductViewControllerRepresentable(isPresentStoreProduct: isPresent, appId: appId) + .frame(width: 0, height: 0) } } +} #endif diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift index 1928817..5b63ee2 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/AppStoreProduct/AppStoreProductViewController.swift @@ -4,62 +4,62 @@ // #if os(iOS) - import StoreKit - import SwiftUI - import UIKit +import StoreKit +import SwiftUI +import UIKit - public class AppStoreProductViewController: UIViewController { - private var isPresentStoreProduct: Binding - private let appId: String +public class AppStoreProductViewController: UIViewController { + private var isPresentStoreProduct: Binding + private let appId: String - public init(isPresentStoreProduct: Binding, appId: String) { - self.isPresentStoreProduct = isPresentStoreProduct - self.appId = appId + public init(isPresentStoreProduct: Binding, appId: String) { + self.isPresentStoreProduct = isPresentStoreProduct + self.appId = appId - super.init(nibName: nil, bundle: nil) - } + super.init(nibName: nil, bundle: nil) + } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - override public func viewDidLoad() { - super.viewDidLoad() - } + override public func viewDidLoad() { + super.viewDidLoad() + } - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } - override public func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - } + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } - func presentStoreProduct() { - let storeProductViewController = SKStoreProductViewController() - storeProductViewController.delegate = self + func presentStoreProduct() { + let storeProductViewController = SKStoreProductViewController() + storeProductViewController.delegate = self - let parameters = [SKStoreProductParameterITunesItemIdentifier: appId] - storeProductViewController.loadProduct(withParameters: parameters) { status, error in - if status { - self.present(storeProductViewController, animated: true, completion: nil) - } else { - if let error { - print("Error: \(error.localizedDescription)") - } + let parameters = [SKStoreProductParameterITunesItemIdentifier: appId] + storeProductViewController.loadProduct(withParameters: parameters) { status, error in + if status { + self.present(storeProductViewController, animated: true, completion: nil) + } else { + if let error { + print("Error: \(error.localizedDescription)") } } + } - DispatchQueue.main.async { - self.isPresentStoreProduct.wrappedValue = false - } + DispatchQueue.main.async { + self.isPresentStoreProduct.wrappedValue = false } } +} - extension AppStoreProductViewController: @preconcurrency SKStoreProductViewControllerDelegate { - public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { - viewController.dismiss(animated: true) - } +extension AppStoreProductViewController: @preconcurrency SKStoreProductViewControllerDelegate { + public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { + viewController.dismiss(animated: true) } +} #endif diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift index b579acd..3a86e4e 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreInstuctinsView.swift @@ -26,49 +26,49 @@ public struct StoreInstuctinsView: View { public var body: some View { ScrollViewReader { value in #if os(iOS) - PageView { offset = $0 } content: { - Group { - switch viewModel.state { - case .initial, .loading: - contentPlaceholder() - case let .result(data): - content(data: data) - case let .error(error): - ErrorView(error) - } + PageView { offset = $0 } content: { + Group { + switch viewModel.state { + case .initial, .loading: + contentPlaceholder() + case let .result(data): + content(data: data) + case let .error(error): + ErrorView(error) } - .paddingContent(.horizontal) - } - .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) - .titleLabel { - PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) - } - .trailingBar { - BarButton(.close) } - .bottomToolbar(style: .none) { - VStack(spacing: .zero) { - StorePaymentButtonBar(trialNotification: true) { - isShowAllPlans = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - withAnimation { - value.scrollTo(10, anchor: .top) - } + .paddingContent(.horizontal) + } + .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) + .titleLabel { + PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) + } + .trailingBar { + BarButton(.close) + } + .bottomToolbar(style: .none) { + VStack(spacing: .zero) { + StorePaymentButtonBar(trialNotification: true) { + isShowAllPlans = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation { + value.scrollTo(10, anchor: .top) } } - .environmentObject(viewModel) - } - } - .onChange(of: isPremium) { _, status in - if status { - dismiss() } + .environmentObject(viewModel) } - .task { - await viewModel.fetchData() + } + .onChange(of: isPremium) { _, status in + if status { + dismiss() } + } + .task { + await viewModel.fetchData() + } #else - EmptyView() + EmptyView() #endif } } @@ -196,28 +196,36 @@ public struct StoreInstuctinsView: View { .padding(.small) .background { Circle() - .fill(LinearGradient(gradient: Gradient( + .fill(LinearGradient( + gradient: Gradient( colors: [Color(hex: "EAAB44"), Color(hex: "D24A44"), Color(hex: "9C5BA2"), Color(hex: "4B5B94")]), - startPoint: .topLeading, endPoint: .bottomTrailing)) + startPoint: .topLeading, + endPoint: .bottomTrailing + )) } - TextBox(title: "Today: Get welcome offer", - subtitle: "Unlock all access to functions", - spacing: .xxxSmall) - .textBoxSize(.small) - .padding(.top, 6) + TextBox( + title: "Today: Get welcome offer", + subtitle: "Unlock all access to functions", + spacing: .xxxSmall + ) + .textBoxSize(.small) + .padding(.top, 6) } HStack { Capsule() - .fill(LinearGradient(gradient: Gradient( + .fill(LinearGradient( + gradient: Gradient( colors: [Color(hex: "EAAB44"), Color(hex: "D24A44"), Color(hex: "9C5BA2")]), - startPoint: .topLeading, endPoint: .trailing)) + startPoint: .topLeading, + endPoint: .trailing + )) .frame(width: 4, height: 15) .padding(.vertical, .xxxSmall) .padding(.leading, .medium) @@ -234,11 +242,13 @@ public struct StoreInstuctinsView: View { .shadowElevaton(.z2) } - TextBox(title: "Day 5", - subtitle: "Get a reminder about when your trial", - spacing: .xxxSmall) - .textBoxSize(.small) - .padding(.top, 6) + TextBox( + title: "Day 5", + subtitle: "Get a reminder about when your trial", + spacing: .xxxSmall + ) + .textBoxSize(.small) + .padding(.top, 6) } HStack { @@ -260,11 +270,13 @@ public struct StoreInstuctinsView: View { .shadowElevaton(.z2) } - TextBox(title: "Day 7", - subtitle: "Tou will be charged on this day, cancel anytime beforel", - spacing: .xxxSmall) - .textBoxSize(.small) - .padding(.top, 6) + TextBox( + title: "Day 7", + subtitle: "Tou will be charged on this day, cancel anytime beforel", + spacing: .xxxSmall + ) + .textBoxSize(.small) + .padding(.top, 6) } } .frame(maxWidth: .infinity) diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift index e2c0f39..57107e3 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreSpecialOfferView.swift @@ -35,24 +35,24 @@ public struct StoreSpecialOfferView: View { public var body: some View { #if os(iOS) - Group { - if #available(iOS 16.0, *) { - newPage - } else { - oldPage - } + Group { + if #available(iOS 16.0, *) { + newPage + } else { + oldPage } + } - .onChange(of: isPremium) { _, status in - if status { - dismiss() - } - } - .task { - await viewModel.fetchData() + .onChange(of: isPremium) { _, status in + if status { + dismiss() } + } + .task { + await viewModel.fetchData() + } #else - EmptyView() + EmptyView() #endif } @@ -169,11 +169,11 @@ public struct StoreSpecialOfferView: View { var imageSize: CGFloat { if screenSize.height > 830 { - return 200 + 200 } else if screenSize.height > 700 { - return 160 + 160 } else { - return 64 + 64 } } @@ -314,33 +314,33 @@ public struct StoreSpecialOfferView: View { var badgeText: String { if let badge = event.badge { - return textPrepere(badge) + textPrepere(badge) } else { - return "" + "" } } var headline: String { if let headline = event.headline { - return textPrepere(headline) + textPrepere(headline) } else { - return "" + "" } } var titleColor: Color { if let accentColor = event.accentColor { - return Color(hex: accentColor) + Color(hex: accentColor) } else { - return Color.onBackgroundPrimary + Color.onBackgroundPrimary } } var description: String { if let description = event.description { - return textPrepere(description) + textPrepere(description) } else { - return "" + "" } } diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift b/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift index 861a886..03faf0e 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/StoreView.swift @@ -13,32 +13,32 @@ import OversizeUI import SwiftUI #if os(iOS) - public struct StoreView: View { - @StateObject private var viewModel: StoreViewModel - @Environment(\.presentationMode) private var presentationMode - @Environment(\.verticalSizeClass) private var verticalSizeClass - @Environment(\.isPortrait) private var isPortrait - private var isClosable = true - @State var isShowFireworks = false - - public init() { - _viewModel = StateObject(wrappedValue: StoreViewModel()) - } +public struct StoreView: View { + @StateObject private var viewModel: StoreViewModel + @Environment(\.presentationMode) private var presentationMode + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.isPortrait) private var isPortrait + private var isClosable = true + @State var isShowFireworks = false + + public init() { + _viewModel = StateObject(wrappedValue: StoreViewModel()) + } - public var body: some View { - Page { - Group { - switch viewModel.state { - case .initial, .loading: - contentPlaceholder() - case let .result(data): - content(data: data) - case let .error(error): - ErrorView(error) - } + public var body: some View { + Page { + Group { + switch viewModel.state { + case .initial, .loading: + contentPlaceholder() + case let .result(data): + content(data: data) + case let .error(error): + ErrorView(error) } - .paddingContent(.horizontal) } + .paddingContent(.horizontal) + } // .backgroundLinerGradient(LinearGradient(colors: [.backgroundPrimary, .backgroundSecondary], startPoint: .top, endPoint: .center)) // .titleLabel { // PremiumLabel(image: Resource.Store.zap, text: Info.store.subscriptionsName, size: .medium) @@ -55,131 +55,131 @@ import SwiftUI // BarButton(.close) // } // } - .bottomToolbar(style: .none) { - if !viewModel.isPremium { - StorePaymentButtonBar() - .environmentObject(viewModel) - } - } - .overlay { - if isShowFireworks { - Fireworks() - } + .bottomToolbar(style: .none) { + if !viewModel.isPremium { + StorePaymentButtonBar() + .environmentObject(viewModel) } - .task { - await viewModel.fetchData() + } + .overlay { + if isShowFireworks { + Fireworks() } } + .task { + await viewModel.fetchData() + } + } - var titleText: String { - if viewModel.isPremium { - return "You are all set!" - } else { - return "Upgrade to \(Info.store.subscriptionsName)" - } + var titleText: String { + if viewModel.isPremium { + "You are all set!" + } else { + "Upgrade to \(Info.store.subscriptionsName)" } + } - var subtitleText: String { - if viewModel.isPremium { - return "Thank you for use to \(Info.store.subscriptionsName).\nHere's what is now unlocked." - } else { - return "Remove ads and unlock all features" - } + var subtitleText: String { + if viewModel.isPremium { + "Thank you for use to \(Info.store.subscriptionsName).\nHere's what is now unlocked." + } else { + "Remove ads and unlock all features" } + } - @ViewBuilder - private func contentPlaceholder() -> some View { - VStack(spacing: .medium) { - VStack(spacing: .xxSmall) { - Text(titleText) - .title() - .foregroundColor(.onSurfacePrimary) - - Text(subtitleText) - .headline() - .foregroundColor(.onSurfaceSecondary) - } - .multilineTextAlignment(.center) + @ViewBuilder + private func contentPlaceholder() -> some View { + VStack(spacing: .medium) { + VStack(spacing: .xxSmall) { + Text(titleText) + .title() + .foregroundColor(.onSurfacePrimary) + + Text(subtitleText) + .headline() + .foregroundColor(.onSurfaceSecondary) + } + .multilineTextAlignment(.center) - HStack(spacing: .xSmall) { - ForEach(0 ..< 3, id: \.self) { _ in - RoundedRectangle(cornerRadius: .small) - .fillSurfaceSecondary() - .frame(height: 180) - } + HStack(spacing: .xSmall) { + ForEach(0 ..< 3, id: \.self) { _ in + RoundedRectangle(cornerRadius: .small) + .fillSurfaceSecondary() + .frame(height: 180) } - - StoreFeaturesView() - .environmentObject(viewModel) } + + StoreFeaturesView() + .environmentObject(viewModel) } + } - @ViewBuilder - private func content(data: StoreKitProducts) -> some View { - VStack(spacing: .medium) { - VStack(spacing: .xxSmall) { - Text(titleText) - .title() - .foregroundColor(.onSurfacePrimary) - - Text(subtitleText) - .headline() - .foregroundColor(.onSurfaceSecondary) - } - .multilineTextAlignment(.center) - - if !viewModel.isPremium { - HStack(spacing: .xSmall) { - ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in - if !product.isOffer { - StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { - viewModel.selectedProduct = product - } - .storeProductStyle(.collumn) - } - } - ForEach(data.nonConsumable) { product in + @ViewBuilder + private func content(data: StoreKitProducts) -> some View { + VStack(spacing: .medium) { + VStack(spacing: .xxSmall) { + Text(titleText) + .title() + .foregroundColor(.onSurfacePrimary) + + Text(subtitleText) + .headline() + .foregroundColor(.onSurfaceSecondary) + } + .multilineTextAlignment(.center) + + if !viewModel.isPremium { + HStack(spacing: .xSmall) { + ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in + if !product.isOffer { StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { viewModel.selectedProduct = product } .storeProductStyle(.collumn) } } + ForEach(data.nonConsumable) { product in + StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { + viewModel.selectedProduct = product + } + .storeProductStyle(.collumn) + } } + } - StoreFeaturesView() - .environmentObject(viewModel) + StoreFeaturesView() + .environmentObject(viewModel) - SubscriptionPrivacyView(products: data) + SubscriptionPrivacyView(products: data) - if !viewModel.isPremium { - productsLust(data: data) - .padding(.bottom, 170) - } + if !viewModel.isPremium { + productsLust(data: data) + .padding(.bottom, 170) } - .onAppear { - Task { - // When this view appears, get the latest subscription status. - await viewModel.updateSubscriptionStatus(products: data) - } + } + .onAppear { + Task { + // When this view appears, get the latest subscription status. + await viewModel.updateSubscriptionStatus(products: data) } - .onChange(of: data.purchasedAutoRenewable) { _, _ in - Task { - // When `purchasedSubscriptions` changes, get the latest subscription status. - await viewModel.updateSubscriptionStatus(products: data) - } + } + .onChange(of: data.purchasedAutoRenewable) { _, _ in + Task { + // When `purchasedSubscriptions` changes, get the latest subscription status. + await viewModel.updateSubscriptionStatus(products: data) } - .onChange(of: viewModel.isPremium) { _, status in - isShowFireworks = status - DispatchQueue.main.asyncAfter(deadline: .now() + 30) { - isShowFireworks = false - } + } + .onChange(of: viewModel.isPremium) { _, status in + isShowFireworks = status + DispatchQueue.main.asyncAfter(deadline: .now() + 30) { + isShowFireworks = false } } + } - @ViewBuilder - private func productsLust(data: StoreKitProducts) -> some View { - VStack(spacing: .small) { + @ViewBuilder + private func productsLust(data: StoreKitProducts) -> some View { + VStack(spacing: .small) { // VStack { // if let currentSubscription = viewModel.currentSubscription { // VStack { @@ -195,47 +195,47 @@ import SwiftUI // } // } - ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in - if !product.isOffer { - StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { - viewModel.selectedProduct = product - } - } - } - ForEach(data.nonConsumable) { product in + ForEach(viewModel.availableSubscriptions /* data.autoRenewable */ ) { product in + if !product.isOffer { StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { viewModel.selectedProduct = product + } + } + } + ForEach(data.nonConsumable) { product in + StoreProductView(product: product, products: data, isSelected: .constant(viewModel.selectedProduct == product)) { + viewModel.selectedProduct = product // Task { // await viewModel.buy(product: product) // } - } } } } + } - public func closable(_ isClosable: Bool = true) -> StoreView { - var control = self - control.isClosable = isClosable - return control - } + public func closable(_ isClosable: Bool = true) -> StoreView { + var control = self + control.isClosable = isClosable + return control } +} - struct StoreView_Previews: PreviewProvider { - static var previews: some View { - StoreView() - } +struct StoreView_Previews: PreviewProvider { + static var previews: some View { + StoreView() } +} #else - public struct StoreView: View { - public init() {} +public struct StoreView: View { + public init() {} - public var body: some View { - Text("Store") - } + public var body: some View { + Text("Store") + } - public func closable(_: Bool = true) -> StoreView { - let control = self - return control - } + public func closable(_: Bool = true) -> StoreView { + let control = self + return control } +} #endif diff --git a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift index 346fe95..d5c7758 100644 --- a/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift +++ b/Sources/OversizeKit/StoreKit/StoreScreen/ViewModel/StoreViewModel.swift @@ -24,7 +24,7 @@ public class StoreViewModel: ObservableObject { @Injected(\.storeKitService) var storeKitService: StoreKitService #if !os(tvOS) - @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol + @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol #endif @Published var state = State.initial @@ -44,9 +44,9 @@ public class StoreViewModel: ObservableObject { var availableSubscriptions: [Product] { if case let .result(products) = state { - return products.autoRenewable.filter { $0.id != currentSubscription?.id } + products.autoRenewable.filter { $0.id != currentSubscription?.id } } else { - return [] + [] } } @@ -132,9 +132,9 @@ extension StoreViewModel { var isHaveSale: Bool { if monthSubscriptionProduct != nil, yearSubscriptionProduct != nil { - return true + true } else { - return false + false } } @@ -305,23 +305,23 @@ extension StoreViewModel { func addTrialNotification(product: Product) async { #if !os(tvOS) - if product.type == .autoRenewable, product.subscription?.introductoryOffer != nil { - do { - try await localNotificationService.requestAuthorization() - if let trialDaysCount = product.trialDaysCount { - let timeInterval = TimeInterval((trialDaysCount - 2) * 24 * 60 * 60) - let notificationTime = Date().addingTimeInterval(timeInterval) - let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) - await localNotificationService.schedule(localNotification: .init( - id: UUID(), - title: "Trial ends soon", - body: "Subscription ends in 2 days", - dateComponents: dateComponents, - repeats: false - )) - } - } catch {} - } + if product.type == .autoRenewable, product.subscription?.introductoryOffer != nil { + do { + try await localNotificationService.requestAuthorization() + if let trialDaysCount = product.trialDaysCount { + let timeInterval = TimeInterval((trialDaysCount - 2) * 24 * 60 * 60) + let notificationTime = Date().addingTimeInterval(timeInterval) + let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) + await localNotificationService.schedule(localNotification: .init( + id: UUID(), + title: "Trial ends soon", + body: "Subscription ends in 2 days", + dateComponents: dateComponents, + repeats: false + )) + } + } catch {} + } #endif } } diff --git a/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift b/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift index 7fa7602..798332b 100644 --- a/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift +++ b/Sources/OversizeKit/StoreKit/ViewModifier/PremiumBlockOverlay.swift @@ -30,10 +30,12 @@ public struct PremiumBlockOverlay: ViewModifier { ZStack { content - LinearGradient(colors: [.surfacePrimary.opacity(0), .surfacePrimary, .surfacePrimary], - startPoint: .top, - endPoint: .bottom) - .ignoresSafeArea() + LinearGradient( + colors: [.surfacePrimary.opacity(0), .surfacePrimary, .surfacePrimary], + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() VStack(spacing: .xxSmall) { VStack(spacing: .xxSmall) { diff --git a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift index 9e71cad..1ca74d8 100644 --- a/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift +++ b/Sources/OversizeKit/StoreKit/Views/BuyButtonStyle.swift @@ -36,7 +36,7 @@ public struct PaymentButtonStyle: ButtonStyle { @Environment(\.controlBorderShape) var controlBorderShape: ControlBorderShape @Environment(\.isBordered) var isBordered: Bool #if !os(tvOS) - @Environment(\.controlSize) var controlSize: ControlSize + @Environment(\.controlSize) var controlSize: ControlSize #endif private let isInfinityWidth: Bool? @@ -84,39 +84,39 @@ public struct PaymentButtonStyle: ButtonStyle { private var horizontalPadding: Space { #if os(tvOS) - return .medium + return .medium #else - switch controlSize { - case .mini: - return .xxSmall - case .small: - return .small - case .regular: - return .small - case .large, .extraLarge: - return .medium - @unknown default: - return .zero - } + switch controlSize { + case .mini: + return .xxSmall + case .small: + return .small + case .regular: + return .small + case .large, .extraLarge: + return .medium + @unknown default: + return .zero + } #endif } private var verticalPadding: Space { #if os(tvOS) - return .medium + return .medium #else - switch controlSize { - case .mini: - return .xxSmall - case .small: - return .xxSmall - case .regular: - return .small - case .large, .extraLarge: - return .medium - @unknown default: - return .zero - } + switch controlSize { + case .mini: + return .xxSmall + case .small: + return .xxSmall + case .regular: + return .small + case .large, .extraLarge: + return .medium + @unknown default: + return .zero + } #endif } @@ -130,15 +130,15 @@ public struct PaymentButtonStyle: ButtonStyle { private var maxWidth: CGFloat? { #if os(tvOS) - return nil + return nil #else - if isInfinityWidth == nil, controlSize == .regular { - return .infinity - } else if let infinity = isInfinityWidth, infinity == true { - return .infinity - } else { - return nil - } + if isInfinityWidth == nil, controlSize == .regular { + return .infinity + } else if let infinity = isInfinityWidth, infinity == true { + return .infinity + } else { + return nil + } #endif } } diff --git a/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift b/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift index 7818f76..126a327 100644 --- a/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift +++ b/Sources/OversizeKit/StoreKit/Views/PrmiumBannerRow.swift @@ -44,25 +44,28 @@ public struct PrmiumBannerRow: View { HStack(spacing: Space.small) { HStack { #if os(iOS) - Resource.Store.zap - .padding(.horizontal, Space.xxSmall) - .padding(.vertical, Space.xxSmall) + Resource.Store.zap + .padding(.horizontal, Space.xxSmall) + .padding(.vertical, Space.xxSmall) #endif #if os(macOS) - Resource.Store.zap - .padding(.horizontal, Space.xxSmall) - .padding(.vertical, Space.xxSmall) + Resource.Store.zap + .padding(.horizontal, Space.xxSmall) + .padding(.vertical, Space.xxSmall) #endif } .background( RoundedRectangle(cornerRadius: Radius.medium.rawValue, style: .continuous) - .fill(LinearGradient(gradient: Gradient( + .fill(LinearGradient( + gradient: Gradient( colors: [Color(hex: "EAAB44"), Color(hex: "D24A44"), Color(hex: "9C5BA2"), Color(hex: "4B5B94")]), - startPoint: .topLeading, endPoint: .bottomTrailing)) + startPoint: .topLeading, + endPoint: .bottomTrailing + )) ) Text(Info.store.subscriptionsName) @@ -99,12 +102,12 @@ public extension PrmiumBannerRow { HStack { HStack(alignment: .center, spacing: Space.xxSmall) { #if os(iOS) - Resource.Store.zap - .colorMultiply(Color(hex: "B75375")) + Resource.Store.zap + .colorMultiply(Color(hex: "B75375")) #endif #if os(macOS) - Resource.Store.zap + Resource.Store.zap #endif Text(Info.store.subscriptionsName) @@ -136,12 +139,15 @@ public extension PrmiumBannerRow { .padding(.vertical, Space.large) .background( RoundedRectangle(cornerRadius: Radius.medium.rawValue, style: .continuous) - .fill(LinearGradient(gradient: Gradient( + .fill(LinearGradient( + gradient: Gradient( colors: [Color(hex: "EAAB44"), Color(hex: "D24A44"), Color(hex: "9C5BA2"), Color(hex: "4B5B94")]), - startPoint: .topLeading, endPoint: .bottomTrailing))) + startPoint: .topLeading, + endPoint: .bottomTrailing + ))) } } diff --git a/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift b/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift index d192629..7cab4b5 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreFeatureDetailView.swift @@ -25,38 +25,38 @@ public struct StoreFeatureDetailView: View { public var body: some View { GeometryReader { geometry in #if os(iOS) - VStack(spacing: .zero) { - TabView(selection: $selection) { - ForEach(Info.store.features) { feature in - fetureItem(feature, geometry: geometry) - .padding(.bottom, isPremium ? .large : .zero) - .tag(feature) - } + VStack(spacing: .zero) { + TabView(selection: $selection) { + ForEach(Info.store.features) { feature in + fetureItem(feature, geometry: geometry) + .padding(.bottom, isPremium ? .large : .zero) + .tag(feature) } - .tabViewStyle(.page(indexDisplayMode: isPremium ? .always : .never)) - .indexViewStyle(.page(backgroundDisplayMode: isPremium ? .always : .never)) + } + .tabViewStyle(.page(indexDisplayMode: isPremium ? .always : .never)) + .indexViewStyle(.page(backgroundDisplayMode: isPremium ? .always : .never)) - if !isPremium { - StorePaymentButtonBar() - .environmentObject(viewModel) - } + if !isPremium { + StorePaymentButtonBar() + .environmentObject(viewModel) } - .overlay(alignment: .topTrailing) { - Button { - dismiss() - } label: { - IconDeprecated( - .xMini, - color: selection.screenURL != nil ? .onPrimary : .onSurfaceTertiary - ) - .padding(.xxSmall) - .background { - Circle() - .fill(.ultraThinMaterial) - } - .padding(.small) + } + .overlay(alignment: .topTrailing) { + Button { + dismiss() + } label: { + IconDeprecated( + .xMini, + color: selection.screenURL != nil ? .onPrimary : .onSurfaceTertiary + ) + .padding(.xxSmall) + .background { + Circle() + .fill(.ultraThinMaterial) } + .padding(.small) } + } #endif } } @@ -85,10 +85,12 @@ public struct StoreFeatureDetailView: View { if let urlString = feature.screenURL, let url = URL(string: urlString) { ScreenMockup(url: url) .frame(maxWidth: 60 + (geometry.size.height * 0.2)) - .padding(feature.topScreenAlignment ?? true ? .top : .bottom, - feature.topScreenAlignment ?? true - ? (geometry.size.height * 0.1) - 24 - : (geometry.size.height * 0.1) + 12) + .padding( + feature.topScreenAlignment ?? true ? .top : .bottom, + feature.topScreenAlignment ?? true + ? (geometry.size.height * 0.1) - 24 + : (geometry.size.height * 0.1) + 12 + ) } } } @@ -159,9 +161,9 @@ public struct StoreFeatureDetailView: View { func backgroundColor(feature: PlistConfiguration.Store.StoreFeature) -> Color { if let color = feature.backgroundColor { - return Color(hex: color) + Color(hex: color) } else { - return Color.accent + Color.accent } } } diff --git a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift index 3b20f80..6096fa1 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesLargeView.swift @@ -31,10 +31,12 @@ struct StoreFeaturesLargeView: View { VStack(spacing: .zero) { RoundedRectangle(cornerRadius: .medium, style: .continuous) .fill( - LinearGradient(gradient: Gradient(colors: [Color(hex: feature.backgroundColor != nil ? feature.backgroundColor : "637DFA"), - Color(hex: feature.backgroundColor != nil ? feature.backgroundColor : "872BFF")]), - startPoint: .topLeading, - endPoint: .bottomTrailing) + LinearGradient( + gradient: Gradient(colors: [Color(hex: feature.backgroundColor != nil ? feature.backgroundColor : "637DFA"), + Color(hex: feature.backgroundColor != nil ? feature.backgroundColor : "872BFF")]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) ) .frame(height: 310) .overlay(alignment: feature.topScreenAlignment ?? true ? .top : .bottom) { @@ -44,8 +46,10 @@ struct StoreFeaturesLargeView: View { if let urlString = feature.screenURL, let url = URL(string: urlString) { ScreenMockup(url: url) .frame(maxWidth: 204) - .padding(feature.topScreenAlignment ?? true ? .top : .bottom, - feature.topScreenAlignment ?? true ? 40 : 70) + .padding( + feature.topScreenAlignment ?? true ? .top : .bottom, + feature.topScreenAlignment ?? true ? 40 : 70 + ) } } } @@ -131,9 +135,9 @@ struct StoreFeaturesLargeView: View { func backgroundColor(feature: PlistConfiguration.Store.StoreFeature) -> Color { if let color = feature.backgroundColor { - return Color(hex: color) + Color(hex: color) } else { - return Color.accent + Color.accent } } } diff --git a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift index c20fc1a..a9bffd3 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreFeaturesView.swift @@ -54,9 +54,9 @@ struct StoreFeaturesView: View { func backgroundColor(feature: PlistConfiguration.Store.StoreFeature) -> Color { if let color = feature.backgroundColor { - return Color(hex: color) + Color(hex: color) } else { - return Color.accent + Color.accent } } } diff --git a/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift b/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift index 42ad0df..b8619cf 100644 --- a/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift +++ b/Sources/OversizeKit/StoreKit/Views/StorePaymentButtonBar.swift @@ -65,14 +65,14 @@ struct StorePaymentButtonBar: View { var backgroundView: some View { Group { #if os(iOS) - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(.ultraThinMaterial) - .overlay { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .strokeBorder(Color.black.opacity(0.05), lineWidth: 0.5) - } + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(.ultraThinMaterial) + .overlay { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .strokeBorder(Color.black.opacity(0.05), lineWidth: 0.5) + } #else - EmptyView() + EmptyView() #endif } } diff --git a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift index a10a81c..580f472 100644 --- a/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift +++ b/Sources/OversizeKit/StoreKit/Views/StoreProductView.swift @@ -29,9 +29,9 @@ public struct StoreProductView: View { var isHaveIntroductoryOffer: Bool { if product.type == .autoRenewable, product.subscription?.introductoryOffer != nil { - return true + true } else { - return false + false } } @@ -41,9 +41,9 @@ public struct StoreProductView: View { var isHaveSale: Bool { if monthSubscriptionProduct != nil, product.subscription?.subscriptionPeriod.unit == .year { - return true + true } else { - return false + false } } @@ -271,22 +271,22 @@ public struct StoreProductView: View { .padding(.vertical, .xxSmall) #if os(iOS) - if isHaveSale, !isPurchased { - Text("Save " + saleProcent + "%") - .caption2(.bold) - .foregroundColor(.onPrimary) - .padding(.vertical, .xxxSmall) - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 2, style: .continuous) - .fill(Color.success) - .cornerRadius(8, corners: [.bottomLeft, .bottomRight]) - } - .padding(.horizontal, 2) - .padding(.bottom, 2) - } + if isHaveSale, !isPurchased { + Text("Save " + saleProcent + "%") + .caption2(.bold) + .foregroundColor(.onPrimary) + .padding(.vertical, .xxxSmall) + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(Color.success) + .cornerRadius(8, corners: [.bottomLeft, .bottomRight]) + } + .padding(.horizontal, 2) + .padding(.bottom, 2) + } #else - EmptyView() + EmptyView() #endif } .frame(maxHeight: .infinity) @@ -322,23 +322,23 @@ public struct StoreProductView: View { var labelBackground: some View { Group { #if os(iOS) - if isHaveIntroductoryOffer, type == .row { - RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(Color.surfacePrimary) - .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) - } else { - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(Color.surfacePrimary) - .overlay { - if type == .collumn, !isSelected { - RoundedRectangle(cornerRadius: 12, style: .continuous) - .strokeBorder(Color.backgroundTertiary, lineWidth: 2) - .padding(-2) - } + if isHaveIntroductoryOffer, type == .row { + RoundedRectangle(cornerRadius: 4, style: .continuous) + .fill(Color.surfacePrimary) + .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) + } else { + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color.surfacePrimary) + .overlay { + if type == .collumn, !isSelected { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(Color.backgroundTertiary, lineWidth: 2) + .padding(-2) } - } + } + } #else - EmptyView() + EmptyView() #endif } } @@ -354,50 +354,50 @@ public struct StoreProductView: View { var backgroundStrokeBorderColor: Color { if isPurchased { - return .success + .success } else if isSelected { - return Palette.blue.color + Palette.blue.color } else { switch type { case .row: - return .backgroundTertiary + .backgroundTertiary case .collumn: - return .surfaceSecondary + .surfaceSecondary } } } var topLabelbackgroundColor: Color { if isPurchased { - return .success + .success } else if isSelected { - return Palette.blue.color + Palette.blue.color } else { - return .surfaceSecondary + .surfaceSecondary } } var topLabelForegroundColor: Color { if isPurchased || isSelected { - return .onPrimary + .onPrimary } else { - return Palette.violet.color + Palette.violet.color } } var descriptionForegroundColor: Color { if isPurchased || product.type != .autoRenewable { - return .onSurfaceSecondary + .onSurfaceSecondary } else { - return .warning + .warning } } var descriptionFontWeight: Font.Weight { if isPurchased || product.type != .autoRenewable { - return .regular + .regular } else { - return .semibold + .semibold } } diff --git a/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift b/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift index 6b8ec96..7a16f08 100644 --- a/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift +++ b/Sources/OversizeKit/StoreKit/Views/SubscriptionPrivacyView.swift @@ -28,42 +28,42 @@ struct SubscriptionPrivacyView: View { .foregroundColor(Color.onSurfaceSecondary) #if os(iOS) - HStack(spacing: .xxSmall) { - Button("Restore") { - Task { - try? await AppStore.sync() - } + HStack(spacing: .xxSmall) { + Button("Restore") { + Task { + try? await AppStore.sync() } + } - Text("•") + Text("•") - if let privacyUrl = Info.url.appPrivacyPolicyUrl { - Button { - isShowPrivacy.toggle() - } label: { - Text("Privacy") - } - .sheet(isPresented: $isShowPrivacy) { - WebView(url: privacyUrl) - } + if let privacyUrl = Info.url.appPrivacyPolicyUrl { + Button { + isShowPrivacy.toggle() + } label: { + Text("Privacy") + } + .sheet(isPresented: $isShowPrivacy) { + WebView(url: privacyUrl) } + } - Text("•") + Text("•") - if let termsOfUde = Info.url.appTermsOfUseUrl { - Button { - isShowTerms.toggle() - } label: { - Text("Terms") - } - .sheet(isPresented: $isShowTerms) { - WebView(url: termsOfUde) - } + if let termsOfUde = Info.url.appTermsOfUseUrl { + Button { + isShowTerms.toggle() + } label: { + Text("Terms") + } + .sheet(isPresented: $isShowTerms) { + WebView(url: termsOfUde) } } - .subheadline(.bold) - .foregroundColor(Color.onSurfaceTertiary) - .padding(.top, .xxxSmall) + } + .subheadline(.bold) + .foregroundColor(Color.onSurfaceTertiary) + .padding(.top, .xxxSmall) #endif } .multilineTextAlignment(.center) diff --git a/Sources/OversizeKit/SystemKit/ErrorView/ErrorView.swift b/Sources/OversizeKit/SystemKit/ErrorView/ErrorView.swift index fa471dc..5724e6c 100644 --- a/Sources/OversizeKit/SystemKit/ErrorView/ErrorView.swift +++ b/Sources/OversizeKit/SystemKit/ErrorView/ErrorView.swift @@ -26,11 +26,13 @@ public struct ErrorView: View { public var body: some View { VStack { Spacer() - ContentView(image: error.image, - title: error.title, - subtitle: error.subtitle, - primaryButton: contenButtonType) - .multilineTextAlignment(.center) + ContentView( + image: error.image, + title: error.title, + subtitle: error.subtitle, + primaryButton: contenButtonType + ) + .multilineTextAlignment(.center) Spacer() } .paddingContent() @@ -42,7 +44,7 @@ public struct ErrorView: View { case .appSettings: return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) case let .tryAgain(action: action): @@ -58,7 +60,7 @@ public struct ErrorView: View { if type == .notAccess || type == .noAccount { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -68,7 +70,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -82,7 +84,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -92,7 +94,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -102,7 +104,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -112,7 +114,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -122,7 +124,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { @@ -132,7 +134,7 @@ public struct ErrorView: View { if type == .notAccess { return .accent(L10n.Button.goToSettings, action: { #if os(iOS) - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) #endif }) } else { diff --git a/Sources/OversizeKit/SystemKit/SuccessView/SuccessView.swift b/Sources/OversizeKit/SystemKit/SuccessView/SuccessView.swift new file mode 100644 index 0000000..286f2a3 --- /dev/null +++ b/Sources/OversizeKit/SystemKit/SuccessView/SuccessView.swift @@ -0,0 +1,117 @@ +// +// Copyright © 2024 Alexander Romanov +// ErrorButtnType.swift, created on 15.11.2024 +// + +import OversizeLocalizable +import OversizeModels +import OversizeResources +import OversizeUI +import SwiftUI + +public struct SuccessView: View where C: View, A: View { + private let image: Image? + private let title: String + private let subtitle: String? + private let closeAction: (() -> Void)? + private let actions: Group? + private let content: C? + + public init( + image: Image? = nil, + title: String, + subtitle: String? = nil, + closeAction: (() -> Void)? = nil, + @ViewBuilder actions: @escaping () -> A, + @ViewBuilder content: () -> C + ) { + self.image = image + self.title = title + self.subtitle = subtitle + self.closeAction = closeAction + self.actions = Group { actions() } + self.content = content() + } + + public var body: some View { + #if os(macOS) + HStack(spacing: .medium) { + VStack(alignment: .center, spacing: .large) { + Spacer() + if let image { + image + .resizable() + .frame(width: 64, height: 64, alignment: .bottom) + } else { + Image.Illustration.Status.success + .resizable() + .frame(width: 64, height: 64, alignment: .bottom) + } + TextBox( + title: title, + subtitle: subtitle, + spacing: .xxSmall + ) + .multilineTextAlignment(.center) + + if actions != nil { + VStack(spacing: .small) { + actions + .controlSize(.large) + } + .frame(width: 200) + } + + Spacer() + } + .frame(maxWidth: .infinity, alignment: .center) + + if let content { + Surface { + content + } + .surfaceClip(true) + .surfaceStyle(.secondary) + .surfaceContentMargins(.zero) + } + } + .paddingContent() + #else + VStack(alignment: .center, spacing: .large) { + Spacer() + if let image { + image + .frame(width: 218, height: 218, alignment: .bottom) + } else { + Illustration.Objects.Check.medium + .frame(width: 218, height: 218, alignment: .bottom) + } + TextBox( + title: title, + subtitle: subtitle, + spacing: .xxSmall + ) + .multilineTextAlignment(.center) + Spacer() + + if let content { + VStack { + content + + Spacer() + } + } + + if actions != nil { + VStack(spacing: .small) { + actions + .controlSize(.large) + } + .padding(.top, .xxSmall) + } + } + .paddingContent() + + #endif + } +} diff --git a/Sources/OversizeLocationKit/AddressField/AddressField.swift b/Sources/OversizeLocationKit/AddressField/AddressField.swift index 2582a24..4ffad85 100644 --- a/Sources/OversizeLocationKit/AddressField/AddressField.swift +++ b/Sources/OversizeLocationKit/AddressField/AddressField.swift @@ -32,26 +32,26 @@ public struct AddressField: View { var isSlectedAddress: Bool { if let seletedAddress, !seletedAddress.isEmpty { - return true + true } else { - return false + false } } var addressText: String { if isSlectedAddress { - return seletedAddress ?? "Address selected" + seletedAddress ?? "Address selected" } else if let seletedLocation { - return "Сoordinates: \(seletedLocation.latitude), \(seletedLocation.longitude)" + "Сoordinates: \(seletedLocation.latitude), \(seletedLocation.longitude)" } else { - return "Address" + "Address" } } public var body: some View { Button { #if !os(watchOS) - isShowPicker.toggle() + isShowPicker.toggle() #endif } label: { VStack(alignment: .leading, spacing: .xSmall) { @@ -98,11 +98,11 @@ public struct AddressField: View { private var fieldOffset: CGFloat { switch fieldPlaceholderPosition { case .default: - return 0 + 0 case .adjacent: - return 0 + 0 case .overInput: - return !isSlectedAddress ? 0 : 10 + !isSlectedAddress ? 0 : 10 } } } diff --git a/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift b/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift index c46b204..6bd633b 100644 --- a/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift +++ b/Sources/OversizeLocationKit/AddressPicker/AddressPicker.swift @@ -12,226 +12,225 @@ import OversizeUI import SwiftUI #if !os(watchOS) - public struct AddressPicker: View { - @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel = AddressPickerViewModel() - @FocusState private var isFocusSearth - - @Binding private var seletedAddress: String? - @Binding private var seletedLocation: CLLocationCoordinate2D? - @Binding private var seletedPlace: LocationAddress? - - public init( - address: Binding = .constant(nil), - location: Binding = .constant(nil), - place: Binding = .constant(nil) - ) { - _seletedAddress = address - _seletedLocation = location - _seletedPlace = place - } +public struct AddressPicker: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel = AddressPickerViewModel() + @FocusState private var isFocusSearth + + @Binding private var seletedAddress: String? + @Binding private var seletedLocation: CLLocationCoordinate2D? + @Binding private var seletedPlace: LocationAddress? + + public init( + address: Binding = .constant(nil), + location: Binding = .constant(nil), + place: Binding = .constant(nil) + ) { + _seletedAddress = address + _seletedLocation = location + _seletedPlace = place + } - public var body: some View { - PageView("Location") { - LazyVStack(spacing: .zero) { - if viewModel.appError != nil { - currentLocation - } + public var body: some View { + PageView("Location") { + LazyVStack(spacing: .zero) { + if viewModel.appError != nil { + currentLocation + } - if viewModel.searchTerm.isEmpty, !viewModel.lastSearchAddresses.isEmpty { - HStack(spacing: .zero) { - Text("Recent") - Spacer() - } - .title3() - .onSurfaceSecondaryForeground() - .padding(.vertical, .xxSmall) - .paddingContent(.horizontal) - - recentResults - } else { - results + if viewModel.searchTerm.isEmpty, !viewModel.lastSearchAddresses.isEmpty { + HStack(spacing: .zero) { + Text("Recent") + Spacer() } + .title3() + .onSurfaceSecondaryForeground() + .padding(.vertical, .xxSmall) + .paddingContent(.horizontal) + + recentResults + } else { + results } } - .leadingBar { - BarButton(.close) - } - .topToolbar { - TextField("Search places or addresses", text: $viewModel.searchTerm) - .submitScope(viewModel.searchTerm.count < 2) - .textFieldStyle(.default) - .focused($isFocusSearth) - .submitLabel(.done) - .onSubmit { - if viewModel.searchTerm.count > 2 { - viewModel.isSaveFromSearth = true - seletedAddress = viewModel.searchTerm - Task { - let coordinate = try? await viewModel.locationService.fetchCoordinateFromAddress(viewModel.searchTerm) - if let coordinate { - let address = try? await viewModel.locationService.fetchAddressFromLocation(coordinate) - seletedLocation = coordinate - seletedPlace = address - } else { - seletedPlace = nil - seletedLocation = nil - } - viewModel.isSaveFromSearth = false - saveToHistory() - dismiss() + } + .leadingBar { + BarButton(.close) + } + .topToolbar { + TextField("Search places or addresses", text: $viewModel.searchTerm) + .submitScope(viewModel.searchTerm.count < 2) + .textFieldStyle(.default) + .focused($isFocusSearth) + .submitLabel(.done) + .onSubmit { + if viewModel.searchTerm.count > 2 { + viewModel.isSaveFromSearth = true + seletedAddress = viewModel.searchTerm + Task { + let coordinate = try? await viewModel.locationService.fetchCoordinateFromAddress(viewModel.searchTerm) + if let coordinate { + let address = try? await viewModel.locationService.fetchAddressFromLocation(coordinate) + seletedLocation = coordinate + seletedPlace = address + } else { + seletedPlace = nil + seletedLocation = nil } + viewModel.isSaveFromSearth = false + saveToHistory() + dismiss() } } - .overlay(alignment: .trailing) { - if viewModel.isSaveFromSearth { - ProgressView() - .padding(.trailing, .xSmall) - } - } - } - // .scrollDismissesKeyboard(.immediately) - .task(priority: .background) { - do { - try await viewModel.updateCurrentPosition() - if viewModel.isSaveCurentPositon { - onSaveCurrntPosition() + } + .overlay(alignment: .trailing) { + if viewModel.isSaveFromSearth { + ProgressView() + .padding(.trailing, .xSmall) } - } catch {} - } - .onAppear { - isFocusSearth = true - } + } } - - private var currentLocation: some View { - Row("Current Location") { - if viewModel.isFetchUpdatePositon { - viewModel.isSaveCurentPositon = true - } else { + // .scrollDismissesKeyboard(.immediately) + .task(priority: .background) { + do { + try await viewModel.updateCurrentPosition() + if viewModel.isSaveCurentPositon { onSaveCurrntPosition() } - } leading: { - IconDeprecated(.navigation) - .iconOnSurface() + } catch {} + } + .onAppear { + isFocusSearth = true + } + } + + private var currentLocation: some View { + Row("Current Location") { + if viewModel.isFetchUpdatePositon { + viewModel.isSaveCurentPositon = true + } else { + onSaveCurrntPosition() } - .padding(.bottom, viewModel.searchTerm.isEmpty ? .small : .zero) - .loading(viewModel.isSaveCurentPositon) + } leading: { + IconDeprecated(.navigation) + .iconOnSurface() } + .padding(.bottom, viewModel.searchTerm.isEmpty ? .small : .zero) + .loading(viewModel.isSaveCurentPositon) + } - private var recentResults: some View { - ForEach(viewModel.lastSearchAddresses.reversed()) { address in + private var recentResults: some View { + ForEach(viewModel.lastSearchAddresses.reversed()) { address in - Row(address.address ?? address.place?.address ?? "Latitude: \(address.location?.latitude ?? 0), longitude:longitude \(address.location?.longitude ?? 0)") { - if let latitude = address.location?.latitude, let longitude = address.location?.longitude { - onCompleteSearth(seletedAddress: address.address, seletedLocation: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), seletedPlace: address.place, saveToHistory: false) - } else { - onCompleteSearth(seletedAddress: address.address, seletedLocation: nil, seletedPlace: address.place, saveToHistory: false) - } - } leading: { - IconDeprecated(.mapPin) - .iconOnSurface() + Row(address.address ?? address.place?.address ?? "Latitude: \(address.location?.latitude ?? 0), longitude:longitude \(address.location?.longitude ?? 0)") { + if let latitude = address.location?.latitude, let longitude = address.location?.longitude { + onCompleteSearth(seletedAddress: address.address, seletedLocation: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), seletedPlace: address.place, saveToHistory: false) + } else { + onCompleteSearth(seletedAddress: address.address, seletedLocation: nil, seletedPlace: address.place, saveToHistory: false) } - .rowClearButton { - if let fooOffset = viewModel.lastSearchAddresses.firstIndex(where: { $0.id == address.id }) { - viewModel.lastSearchAddresses.remove(at: fooOffset) - } + } leading: { + IconDeprecated(.mapPin) + .iconOnSurface() + } + .rowClearButton { + if let fooOffset = viewModel.lastSearchAddresses.firstIndex(where: { $0.id == address.id }) { + viewModel.lastSearchAddresses.remove(at: fooOffset) } } } + } - private var results: some View { - ForEach(viewModel.locationResults, id: \.self) { location in + private var results: some View { + ForEach(viewModel.locationResults, id: \.self) { location in - Row(location.title, subtitle: location.subtitle) { - reverseGeo(location: location) - } leading: { - IconDeprecated(.mapPin) - .iconOnSurface() - } + Row(location.title, subtitle: location.subtitle) { + reverseGeo(location: location) + } leading: { + IconDeprecated(.mapPin) + .iconOnSurface() } } + } - func reverseGeo(location: MKLocalSearchCompletion) { - let searchRequest = MKLocalSearch.Request(completion: location) - let search = MKLocalSearch(request: searchRequest) - var coordinateK: CLLocationCoordinate2D? - search.start { response, error in - if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate { - coordinateK = coordinate - } + func reverseGeo(location: MKLocalSearchCompletion) { + let searchRequest = MKLocalSearch.Request(completion: location) + let search = MKLocalSearch(request: searchRequest) + var coordinateK: CLLocationCoordinate2D? + search.start { response, error in + if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate { + coordinateK = coordinate + } - if let c = coordinateK { - let location = CLLocation(latitude: c.latitude, longitude: c.longitude) - CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in + if let c = coordinateK { + let location = CLLocation(latitude: c.latitude, longitude: c.longitude) + CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in - guard let placemark = placemarks?.first else { - let errorString = error?.localizedDescription ?? "Unexpected Error" - print("Unable to reverse geocode, \(errorString)") - return - } + guard let placemark = placemarks?.first else { + let errorString = error?.localizedDescription ?? "Unexpected Error" + print("Unable to reverse geocode, \(errorString)") + return + } - let reversedGeoLocation = LocationAddress(with: placemark) + let reversedGeoLocation = LocationAddress(with: placemark) - let address = "\(reversedGeoLocation.streetName) \(reversedGeoLocation.streetNumber)".capitalizingFirstLetter() - onCompleteSearth(seletedAddress: address, seletedLocation: c, seletedPlace: reversedGeoLocation) - } + let address = "\(reversedGeoLocation.streetName) \(reversedGeoLocation.streetNumber)".capitalizingFirstLetter() + onCompleteSearth(seletedAddress: address, seletedLocation: c, seletedPlace: reversedGeoLocation) } } } + } - func onCompleteSearth(seletedAddress: String?, seletedLocation: CLLocationCoordinate2D?, seletedPlace: LocationAddress?, saveToHistory: Bool = true) { - if let seletedAddress { - self.seletedAddress = seletedAddress - } else { - self.seletedAddress = seletedPlace?.address - } - self.seletedLocation = seletedLocation - self.seletedPlace = seletedPlace - if saveToHistory { - self.saveToHistory() - } - dismiss() + func onCompleteSearth(seletedAddress: String?, seletedLocation: CLLocationCoordinate2D?, seletedPlace: LocationAddress?, saveToHistory: Bool = true) { + if let seletedAddress { + self.seletedAddress = seletedAddress + } else { + self.seletedAddress = seletedPlace?.address } - - private func onSaveCurrntPosition() { - Task { - let address = try? await viewModel.locationService.fetchAddressFromLocation(viewModel.currentLocation) - if let address { - seletedAddress = address.address - seletedPlace = address - } else { - seletedAddress = nil - seletedPlace = nil - } - seletedLocation = viewModel.currentLocation - saveToHistory() - dismiss() - } + self.seletedLocation = seletedLocation + self.seletedPlace = seletedPlace + if saveToHistory { + self.saveToHistory() } + dismiss() + } - func saveToHistory() { - let lastSearth: SearchHistoryAddress - if let seletedLocation { - lastSearth = SearchHistoryAddress( - id: UUID().uuidString, - address: seletedAddress, - location: SearchHistoryLocationCoordinate(coordinate: seletedLocation), - place: seletedPlace - ) + private func onSaveCurrntPosition() { + Task { + let address = try? await viewModel.locationService.fetchAddressFromLocation(viewModel.currentLocation) + if let address { + seletedAddress = address.address + seletedPlace = address } else { - lastSearth = SearchHistoryAddress( - id: UUID().uuidString, - address: seletedAddress, - location: nil, - place: seletedPlace - ) + seletedAddress = nil + seletedPlace = nil } - viewModel.lastSearchAddresses.append(lastSearth) + seletedLocation = viewModel.currentLocation + saveToHistory() + dismiss() } + } - private func onDoneAction() { - dismiss() + func saveToHistory() { + let lastSearth = if let seletedLocation { + SearchHistoryAddress( + id: UUID().uuidString, + address: seletedAddress, + location: SearchHistoryLocationCoordinate(coordinate: seletedLocation), + place: seletedPlace + ) + } else { + SearchHistoryAddress( + id: UUID().uuidString, + address: seletedAddress, + location: nil, + place: seletedPlace + ) } + viewModel.lastSearchAddresses.append(lastSearth) + } + + private func onDoneAction() { + dismiss() } +} #endif diff --git a/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift b/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift index 6db6c07..d6d5b23 100644 --- a/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift +++ b/Sources/OversizeLocationKit/AddressPicker/AddressPickerViewModel.swift @@ -12,76 +12,76 @@ import OversizeModels import SwiftUI #if !os(watchOS) - @MainActor - class AddressPickerViewModel: NSObject, ObservableObject { - @Injected(\.locationService) var locationService: LocationServiceProtocol +@MainActor +class AddressPickerViewModel: NSObject, ObservableObject { + @Injected(\.locationService) var locationService: LocationServiceProtocol - @Published var locationResults: [MKLocalSearchCompletion] = .init() - @Published var searchTerm: String = .init() - @AppStorage("AppState.LastSearchAddresses") var lastSearchAddresses: [SearchHistoryAddress] = .init() + @Published var locationResults: [MKLocalSearchCompletion] = .init() + @Published var searchTerm: String = .init() + @AppStorage("AppState.LastSearchAddresses") var lastSearchAddresses: [SearchHistoryAddress] = .init() - @Published var currentLocation: CLLocationCoordinate2D = .init(latitude: 0, longitude: 0) + @Published var currentLocation: CLLocationCoordinate2D = .init(latitude: 0, longitude: 0) - @Published var isFetchUpdatePositon: Bool = .init(false) - @Published var isSaveCurentPositon: Bool = .init(false) - @Published var isSaveFromSearth: Bool = .init(false) + @Published var isFetchUpdatePositon: Bool = .init(false) + @Published var isSaveCurentPositon: Bool = .init(false) + @Published var isSaveFromSearth: Bool = .init(false) - private var cancellables: Set = [] + private var cancellables: Set = [] - private var searchCompleter = MKLocalSearchCompleter() - private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)? + private var searchCompleter = MKLocalSearchCompleter() + private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)? - @State var appError: AppError? + @State var appError: AppError? - override init() { - super.init() - searchCompleter.delegate = self - searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address]) + override init() { + super.init() + searchCompleter.delegate = self + searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address]) - $searchTerm - .debounce(for: .seconds(0.2), scheduler: RunLoop.main) - .removeDuplicates() - .flatMap { currentSearchTerm in - self.searchTermToResults(searchTerm: currentSearchTerm) - } - .sink(receiveCompletion: { _ in - // Handle error - }, receiveValue: { results in - self.locationResults = results - }) - .store(in: &cancellables) - } - - func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> { - Future { promise in - self.searchCompleter.queryFragment = searchTerm - self.currentPromise = promise + $searchTerm + .debounce(for: .seconds(0.2), scheduler: RunLoop.main) + .removeDuplicates() + .flatMap { currentSearchTerm in + self.searchTermToResults(searchTerm: currentSearchTerm) } - } + .sink(receiveCompletion: { _ in + // Handle error + }, receiveValue: { results in + self.locationResults = results + }) + .store(in: &cancellables) } - extension AddressPickerViewModel: @preconcurrency MKLocalSearchCompleterDelegate { - func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { - currentPromise?(.success(completer.results)) + func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> { + Future { promise in + self.searchCompleter.queryFragment = searchTerm + self.currentPromise = promise } + } +} - func completer(_: MKLocalSearchCompleter, didFailWithError _: Error) {} +extension AddressPickerViewModel: @preconcurrency MKLocalSearchCompleterDelegate { + func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { + currentPromise?(.success(completer.results)) } - extension AddressPickerViewModel { - func updateCurrentPosition() async throws { - let status = locationService.permissionsStatus() - switch status { - case .success: - isFetchUpdatePositon = true - let currentPosition = try await locationService.currentLocation() - guard let newLocation = currentPosition else { return } - currentLocation = newLocation - print("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") - isFetchUpdatePositon = false - case let .failure(error): - appError = error - } + func completer(_: MKLocalSearchCompleter, didFailWithError _: Error) {} +} + +extension AddressPickerViewModel { + func updateCurrentPosition() async throws { + let status = locationService.permissionsStatus() + switch status { + case .success: + isFetchUpdatePositon = true + let currentPosition = try await locationService.currentLocation() + guard let newLocation = currentPosition else { return } + currentLocation = newLocation + print("📍 Location: \(newLocation.latitude), \(newLocation.longitude)") + isFetchUpdatePositon = false + case let .failure(error): + appError = error } } +} #endif diff --git a/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift b/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift index e36b40d..cd525b3 100644 --- a/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift +++ b/Sources/OversizeLocationKit/MapCoordinateView/MapCoordinateView.swift @@ -8,155 +8,155 @@ import OversizeUI import SwiftUI #if !os(watchOS) - public struct MapCoordinateView: View { - @Environment(\.screenSize) var screenSize - @Environment(\.openURL) var openURL - @StateObject var viewModel: MapCoordinateViewModel +public struct MapCoordinateView: View { + @Environment(\.screenSize) var screenSize + @Environment(\.openURL) var openURL + @StateObject var viewModel: MapCoordinateViewModel - public init(_ location: CLLocationCoordinate2D, annotation: String? = nil) { - _viewModel = StateObject(wrappedValue: MapCoordinateViewModel(location: location, annotation: annotation)) - } + public init(_ location: CLLocationCoordinate2D, annotation: String? = nil) { + _viewModel = StateObject(wrappedValue: MapCoordinateViewModel(location: location, annotation: annotation)) + } - public var body: some View { - VStack(spacing: 0) { - if #available(iOS 16.0, *) { - mapView - .ignoresSafeArea() - .safeAreaInset(edge: .top) { - ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { - BarButton(.back) - }, trailingBar: { - BarButton(.icon(.map, action: { - viewModel.isShowRoutePickerSheet.toggle() - })) - }) - .background(.thickMaterial, ignoresSafeAreaEdges: .top) - } - #if os(iOS) - .toolbar(.hidden, for: .tabBar) - #endif - } else { - mapView - .safeAreaInset(edge: .top) { - ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { - BarButton(.back) - }, trailingBar: { - BarButton(.icon(.map, action: { - viewModel.isShowRoutePickerSheet.toggle() - })) - }) - } - } - } - .sheet(isPresented: $viewModel.isShowRoutePickerSheet) { - routeSheetView - .presentationDetents([.height(260)]) + public var body: some View { + VStack(spacing: 0) { + if #available(iOS 16.0, *) { + mapView + .ignoresSafeArea() + .safeAreaInset(edge: .top) { + ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { + BarButton(.back) + }, trailingBar: { + BarButton(.icon(.map, action: { + viewModel.isShowRoutePickerSheet.toggle() + })) + }) + .background(.thickMaterial, ignoresSafeAreaEdges: .top) + } + #if os(iOS) + .toolbar(.hidden, for: .tabBar) + #endif + } else { + mapView + .safeAreaInset(edge: .top) { + ModalNavigationBar(title: viewModel.annotation ?? "", largeTitle: false, leadingBar: { + BarButton(.back) + }, trailingBar: { + BarButton(.icon(.map, action: { + viewModel.isShowRoutePickerSheet.toggle() + })) + }) + } } } + .sheet(isPresented: $viewModel.isShowRoutePickerSheet) { + routeSheetView + .presentationDetents([.height(260)]) + } + } - var mapView: some View { - ZStack(alignment: .trailing) { - Map(coordinateRegion: region, showsUserLocation: true, userTrackingMode: $viewModel.userTrackingMode, annotationItems: viewModel.annotations) { - MapMarker(coordinate: $0.coordinate) - } - controlButtons + var mapView: some View { + ZStack(alignment: .trailing) { + Map(coordinateRegion: region, showsUserLocation: true, userTrackingMode: $viewModel.userTrackingMode, annotationItems: viewModel.annotations) { + MapMarker(coordinate: $0.coordinate) } + controlButtons } + } - private var region: Binding { - Binding { - viewModel.region - } set: { region in - DispatchQueue.main.async { - viewModel.region = region - } + private var region: Binding { + Binding { + viewModel.region + } set: { region in + DispatchQueue.main.async { + viewModel.region = region } } + } - var controlButtons: some View { - VStack { - Spacer() - VStack(spacing: .zero) { - Button { - viewModel.zoomIn() - } label: { - IconDeprecated(.plus) - .onSurfaceSecondaryForeground() - .padding(.xxSmall) - } - - Button { - viewModel.zoomOut() - } label: { - IconDeprecated(.minus) - .onSurfaceSecondaryForeground() - .padding(.xxSmall) - } - } - .background { - Capsule() - .fillSurfacePrimary() - .shadowElevaton(.z1) - } - Spacer() - } - .overlay(alignment: .bottomTrailing, content: { + var controlButtons: some View { + VStack { + Spacer() + VStack(spacing: .zero) { Button { - viewModel.positionInLocation() - + viewModel.zoomIn() } label: { - IconDeprecated(.navigation) + IconDeprecated(.plus) .onSurfaceSecondaryForeground() .padding(.xxSmall) } - .background { - Capsule() - .fillSurfacePrimary() - .shadowElevaton(.z1) + + Button { + viewModel.zoomOut() + } label: { + IconDeprecated(.minus) + .onSurfaceSecondaryForeground() + .padding(.xxSmall) } - }) - .padding(.trailing, 16) - .padding(.bottom, screenSize.safeAreaBottom) + } + .background { + Capsule() + .fillSurfacePrimary() + .shadowElevaton(.z1) + } + Spacer() } + .overlay(alignment: .bottomTrailing, content: { + Button { + viewModel.positionInLocation() - var routeSheetView: some View { - PageView("Route") { - SectionView { - Row("Apple Maps") { - onTapAppleMaps() - } - Row("Google Maps") { - onTapGoogleMaps() - } + } label: { + IconDeprecated(.navigation) + .onSurfaceSecondaryForeground() + .padding(.xxSmall) + } + .background { + Capsule() + .fillSurfacePrimary() + .shadowElevaton(.z1) + } + }) + .padding(.trailing, 16) + .padding(.bottom, screenSize.safeAreaBottom) + } + + var routeSheetView: some View { + PageView("Route") { + SectionView { + Row("Apple Maps") { + onTapAppleMaps() + } + Row("Google Maps") { + onTapGoogleMaps() } } - .leadingBar(leadingBar: { - BarButton(.close) - }) - .backgroundSecondary() - .disableScrollShadow(true) - .surfaceContentRowMargins() } + .leadingBar(leadingBar: { + BarButton(.close) + }) + .backgroundSecondary() + .disableScrollShadow(true) + .surfaceContentRowMargins() + } - func onTapAppleMaps() { - #if !os(tvOS) - let placemark = MKPlacemark(coordinate: viewModel.location, addressDictionary: nil) - let mapItem = MKMapItem(placemark: placemark) - mapItem.name = viewModel.annotation - mapItem.openInMaps() - viewModel.isShowRoutePickerSheet.toggle() - #endif - } + func onTapAppleMaps() { + #if !os(tvOS) + let placemark = MKPlacemark(coordinate: viewModel.location, addressDictionary: nil) + let mapItem = MKMapItem(placemark: placemark) + mapItem.name = viewModel.annotation + mapItem.openInMaps() + viewModel.isShowRoutePickerSheet.toggle() + #endif + } - func onTapGoogleMaps() { - guard let url = URL(string: "comgooglemaps://?saddr=\(viewModel.location.latitude),\(viewModel.location.longitude)") else { return } - openURL(url) - } + func onTapGoogleMaps() { + guard let url = URL(string: "comgooglemaps://?saddr=\(viewModel.location.latitude),\(viewModel.location.longitude)") else { return } + openURL(url) } +} - struct MapCoordinateView_Previews: PreviewProvider { - static var previews: some View { - MapCoordinateView(.init(latitude: 100, longitude: 100)) - } +struct MapCoordinateView_Previews: PreviewProvider { + static var previews: some View { + MapCoordinateView(.init(latitude: 100, longitude: 100)) } +} #endif diff --git a/Sources/OversizeNoticeKit/NoticeListViewModel.swift b/Sources/OversizeNoticeKit/NoticeListViewModel.swift index 0862c48..1749bd5 100644 --- a/Sources/OversizeNoticeKit/NoticeListViewModel.swift +++ b/Sources/OversizeNoticeKit/NoticeListViewModel.swift @@ -92,9 +92,9 @@ public final class NoticeListViewModel: ObservableObject { private func checkDateInSelectedPeriod(startDate: Date, endDate: Date) -> Bool { if startDate < endDate { - return (startDate ... endDate).contains(Date()) + (startDate ... endDate).contains(Date()) } else { - return false + false } } diff --git a/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift b/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift index 94d24ed..2f0a503 100644 --- a/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift +++ b/Sources/OversizeNotificationKit/LocalNotificationSetScreenViewModel.swift @@ -10,76 +10,76 @@ import OversizeNotificationService import SwiftUI #if !os(tvOS) - @MainActor - class LocalNotificationSetScreenViewModel: ObservableObject { - @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol - @Published var state = State.initial +@MainActor +class LocalNotificationSetScreenViewModel: ObservableObject { + @Injected(\.localNotificationService) var localNotificationService: LocalNotificationServiceProtocol + @Published var state = State.initial - public let id: UUID - private let date: Date - private let title: String - private let body: String - private let userInfo: [AnyHashable: Any]? + public let id: UUID + private let date: Date + private let title: String + private let body: String + private let userInfo: [AnyHashable: Any]? - init( - id: UUID, - date: Date, - title: String, - body: String, - userInfo: [AnyHashable: Any]? = nil - ) { - self.id = id - self.date = date - self.title = title - self.body = body - self.userInfo = userInfo - } + init( + id: UUID, + date: Date, + title: String, + body: String, + userInfo: [AnyHashable: Any]? = nil + ) { + self.id = id + self.date = date + self.title = title + self.body = body + self.userInfo = userInfo + } - func setNotification(timeBefore: LocalNotificationTime) async { - let notificationTime = date.addingTimeInterval(timeBefore.timeInterval) - let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) + func setNotification(timeBefore: LocalNotificationTime) async { + let notificationTime = date.addingTimeInterval(timeBefore.timeInterval) + let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationTime) - let stringUserInfo = userInfo?.reduce(into: [String: String]()) { result, pair in - if let key = pair.key as? String, let value = pair.value as? String { - result[key] = value - } + let stringUserInfo = userInfo?.reduce(into: [String: String]()) { result, pair in + if let key = pair.key as? String, let value = pair.value as? String { + result[key] = value } - - await localNotificationService.schedule(localNotification: .init( - id: id, - title: title, - body: body, - dateComponents: dateComponents, - repeats: false, - userInfo: stringUserInfo - )) } - func fetchPandingNotification() async -> Bool { - let ids = await localNotificationService.fetchPendingIds() - return ids.contains(id.uuidString) - } + await localNotificationService.schedule(localNotification: .init( + id: id, + title: title, + body: body, + dateComponents: dateComponents, + repeats: false, + userInfo: stringUserInfo + )) + } - func deleteNotification() { - localNotificationService.removeRequest(withIdentifier: id.uuidString) - } + func fetchPandingNotification() async -> Bool { + let ids = await localNotificationService.fetchPendingIds() + return ids.contains(id.uuidString) + } - func requestAccsess() async { - let result = await localNotificationService.requestAccess() - switch result { - case .success: - state = .result - case let .failure(error): - state = .error(error) - } - } + func deleteNotification() { + localNotificationService.removeRequest(withIdentifier: id.uuidString) } - extension LocalNotificationSetScreenViewModel { - enum State { - case initial - case result - case error(AppError) + func requestAccsess() async { + let result = await localNotificationService.requestAccess() + switch result { + case .success: + state = .result + case let .failure(error): + state = .error(error) } } +} + +extension LocalNotificationSetScreenViewModel { + enum State { + case initial + case result + case error(AppError) + } +} #endif diff --git a/Sources/OversizeNotificationKit/LocalNotificationView.swift b/Sources/OversizeNotificationKit/LocalNotificationView.swift index e574636..206c842 100644 --- a/Sources/OversizeNotificationKit/LocalNotificationView.swift +++ b/Sources/OversizeNotificationKit/LocalNotificationView.swift @@ -10,107 +10,107 @@ import SwiftUI import UserNotifications #if !os(tvOS) - public struct LocalNotificationView: View { - @Environment(\.dismiss) var dismiss - @Binding private var selection: LocalNotificationTime - @State private var isPendingNotification: Bool = false - @StateObject var viewModel: LocalNotificationSetScreenViewModel - private let saveAction: ((UUID?) -> Void)? +public struct LocalNotificationView: View { + @Environment(\.dismiss) var dismiss + @Binding private var selection: LocalNotificationTime + @State private var isPendingNotification: Bool = false + @StateObject var viewModel: LocalNotificationSetScreenViewModel + private let saveAction: ((UUID?) -> Void)? - public init( - _ selection: Binding, - id: UUID, - title: String, - body: String, - date: Date, - userInfo: [AnyHashable: Any]? = nil, - saveAction: ((UUID?) -> Void)? = nil - ) { - _viewModel = StateObject(wrappedValue: LocalNotificationSetScreenViewModel( - id: id, - date: date, - title: title, - body: body, - userInfo: userInfo - )) - _selection = selection - self.saveAction = saveAction - } + public init( + _ selection: Binding, + id: UUID, + title: String, + body: String, + date: Date, + userInfo: [AnyHashable: Any]? = nil, + saveAction: ((UUID?) -> Void)? = nil + ) { + _viewModel = StateObject(wrappedValue: LocalNotificationSetScreenViewModel( + id: id, + date: date, + title: title, + body: body, + userInfo: userInfo + )) + _selection = selection + self.saveAction = saveAction + } - public var body: some View { - switch viewModel.state { - case .initial: - contnent - .task { - await viewModel.requestAccsess() - } - case .result: - contnent - .task { - let pendingStatus = await viewModel.fetchPandingNotification() - if pendingStatus { - isPendingNotification = true - } - } - case let .error(error): - PageView("Notification") { - ErrorView(error) + public var body: some View { + switch viewModel.state { + case .initial: + contnent + .task { + await viewModel.requestAccsess() } - .leadingBar { - BarButton(.close) + case .result: + contnent + .task { + let pendingStatus = await viewModel.fetchPandingNotification() + if pendingStatus { + isPendingNotification = true + } } + case let .error(error): + PageView("Notification") { + ErrorView(error) + } + .leadingBar { + BarButton(.close) } } + } - public var contnent: some View { - // let notificationDate = viewModel.date.addingTimeInterval(selection.timeInterval) - PageView("Notification") { - VStack(spacing: .zero) { - SectionView { - LazyVStack(spacing: .zero) { - ForEach(LocalNotificationTime.allCases) { notificationTime in + public var contnent: some View { + // let notificationDate = viewModel.date.addingTimeInterval(selection.timeInterval) + PageView("Notification") { + VStack(spacing: .zero) { + SectionView { + LazyVStack(spacing: .zero) { + ForEach(LocalNotificationTime.allCases) { notificationTime in // let notificationDate = viewModel.date.addingTimeInterval(notificationTime.timeInterval) // if notificationDate viewModel.date { - Radio(notificationTime.title, isOn: selection.id == notificationTime.id) { - selection = notificationTime - } - // } + Radio(notificationTime.title, isOn: selection.id == notificationTime.id) { + selection = notificationTime } + // } } } - .surfaceContentRowMargins() - if isPendingNotification { - SectionView { - VStack(spacing: .zero) { - Row("Delete notification") { - viewModel.deleteNotification() - saveAction?(nil) - isPendingNotification = false - dismiss() - } trailing: { - IconDeprecated(.trash) - .iconColor(Color.error) - } + } + .surfaceContentRowMargins() + if isPendingNotification { + SectionView { + VStack(spacing: .zero) { + Row("Delete notification") { + viewModel.deleteNotification() + saveAction?(nil) + isPendingNotification = false + dismiss() + } trailing: { + IconDeprecated(.trash) + .iconColor(Color.error) } } - .surfaceContentRowMargins() } + .surfaceContentRowMargins() } } - .backgroundSecondary() - .leadingBar { - BarButton(.close) - } - .trailingBar { - BarButton(.accent("Done", action: { - Task { - await viewModel.setNotification(timeBefore: selection) - saveAction?(viewModel.id) - isPendingNotification = true - dismiss() - } - })) - } + } + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + Task { + await viewModel.setNotification(timeBefore: selection) + saveAction?(viewModel.id) + isPendingNotification = true + dismiss() + } + })) } } +} #endif diff --git a/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift b/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift index 3ace115..6fa17ac 100644 --- a/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift +++ b/Sources/OversizeNotificationKit/Model/LocalNotificationAlertsTimes.swift @@ -11,46 +11,46 @@ public enum LocalNotificationTime: CaseIterable, Equatable, Identifiable, @unche public var title: String { switch self { case .oneMinuteBefore: - return "1 minute before" + "1 minute before" case .fiveMinutesBefore: - return "5 minutes before" + "5 minutes before" case .tenMinutesBefore: - return "10 minutes before" + "10 minutes before" case .thirtyMinutesBefore: - return "30 minutes before" + "30 minutes before" case .oneHourBefore: - return "1 hour before" + "1 hour before" case .twoHoursBefore: - return "2 hours before" + "2 hours before" case .oneDayBefore: - return "1 day before" + "1 day before" case .twoDaysBefore: - return "2 days before" + "2 days before" case .oneWeekBefore: - return "1 week before" + "1 week before" } } public var timeInterval: TimeInterval { switch self { case .oneMinuteBefore: - return -1 * 60 + -1 * 60 case .fiveMinutesBefore: - return -5 * 60 + -5 * 60 case .tenMinutesBefore: - return -10 * 60 + -10 * 60 case .thirtyMinutesBefore: - return -30 * 60 + -30 * 60 case .oneHourBefore: - return -1 * 60 * 60 + -1 * 60 * 60 case .twoHoursBefore: - return -2 * 60 * 60 + -2 * 60 * 60 case .oneDayBefore: - return -1 * 24 * 60 * 60 + -1 * 24 * 60 * 60 case .twoDaysBefore: - return -2 * 24 * 60 * 60 + -2 * 24 * 60 * 60 case .oneWeekBefore: - return -7 * 24 * 60 * 60 + -7 * 24 * 60 * 60 } } diff --git a/Sources/OversizeOnboardingKit/OnboardingView.swift b/Sources/OversizeOnboardingKit/OnboardingView.swift index 9b0235b..48974fc 100644 --- a/Sources/OversizeOnboardingKit/OnboardingView.swift +++ b/Sources/OversizeOnboardingKit/OnboardingView.swift @@ -41,17 +41,17 @@ public struct OnboardView: View where A: View, C: View { private func topButtons() -> some View { HStack { #if os(iOS) - if helpAction != nil { - Button { - helpAction?() - } label: { - Text("Help") - } - .buttonStyle(.tertiary) - .controlBorderShape(.capsule) - .accent() - .controlSize(.mini) + if helpAction != nil { + Button { + helpAction?() + } label: { + Text("Help") } + .buttonStyle(.tertiary) + .controlBorderShape(.capsule) + .accent() + .controlSize(.mini) + } #endif Spacer() @@ -75,57 +75,57 @@ public struct OnboardView: View where A: View, C: View { private func bottomButtons() -> some View { #if os(iOS) - HStack(spacing: .small) { - if let backAction { - Button { - backAction() - } label: { - Image.Base.arrowLeft.icon() - } - .buttonStyle(.quaternary) - .accentColor(.secondary) + HStack(spacing: .small) { + if let backAction { + Button { + backAction() + } label: { + Image.Base.arrowLeft.icon() } + .buttonStyle(.quaternary) + .accentColor(.secondary) + } - VStack(spacing: .xxxSmall) { - actions - } + VStack(spacing: .xxxSmall) { + actions } - .padding(.medium) + } + .padding(.medium) #else - HStack(spacing: .xSmall) { - if let helpAction { - Button("Help", action: helpAction) - .help("Help") - #if !os(tvOS) - .controlSize(.extraLarge) - .buttonStyle(.bordered) - #endif - } - - Spacer() - - if let backAction { - Button( - "Back", - action: backAction - ) - #if !os(tvOS) + HStack(spacing: .xSmall) { + if let helpAction { + Button("Help", action: helpAction) + .help("Help") + #if !os(tvOS) .controlSize(.extraLarge) - #endif .buttonStyle(.bordered) - } + #endif + } - actions + Spacer() + + if let backAction { + Button( + "Back", + action: backAction + ) #if !os(tvOS) .controlSize(.extraLarge) #endif - .buttonStyle(.borderedProminent) - } - .padding(.small) - .background(Color.surfacePrimary) - .overlay(alignment: .top) { - Separator() + .buttonStyle(.bordered) } + + actions + #if !os(tvOS) + .controlSize(.extraLarge) + #endif + .buttonStyle(.borderedProminent) + } + .padding(.small) + .background(Color.surfacePrimary) + .overlay(alignment: .top) { + Separator() + } #endif } } diff --git a/Sources/OversizePhotoKit/PhotoOptionsView.swift b/Sources/OversizePhotoKit/PhotoOptionsView.swift index 904582c..9e68b6d 100644 --- a/Sources/OversizePhotoKit/PhotoOptionsView.swift +++ b/Sources/OversizePhotoKit/PhotoOptionsView.swift @@ -64,19 +64,19 @@ public struct PhotoOptionsView: View where A: View { SectionView { VStack { #if !os(tvOS) - if #available(iOS 16.0, *) { - ShareLink( - item: photo, - preview: SharePreview( - "Photo", - image: photo.image - ) - ) { - Row("Share") { - Image.Base.upload - } + if #available(iOS 16.0, *) { + ShareLink( + item: photo, + preview: SharePreview( + "Photo", + image: photo.image + ) + ) { + Row("Share") { + Image.Base.upload } } + } #endif actions }