From f290c3513c9d8ba62ca9424fed27c763c0634312 Mon Sep 17 00:00:00 2001 From: Alexander Romanov Date: Wed, 26 Jul 2023 08:22:28 +0300 Subject: [PATCH] Add DateField, PhoneField, PriceField amd URLField --- .github/workflows/bump.yml | 1 - .../OversizeUI/Controls/Avatar/Avatar.swift | 1 + .../Controls/DateField/DateField.swift | 93 ++++++++++++++++ .../Controls/DateField/DatePickerSheet.swift | 70 ++++++++++++ .../Controls/PhoneField/PhoneField.swift | 31 ++++++ .../Controls/PriceField/PriceField.swift | 37 +++++++ .../Controls/URLField/URLField.swift | 42 ++++++++ .../ViewModifier/HalfSheet/HalfSheet.swift | 102 ++++++++++++++++++ 8 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 Sources/OversizeUI/Controls/DateField/DateField.swift create mode 100644 Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift create mode 100644 Sources/OversizeUI/Controls/PhoneField/PhoneField.swift create mode 100644 Sources/OversizeUI/Controls/PriceField/PriceField.swift create mode 100644 Sources/OversizeUI/Controls/URLField/URLField.swift create mode 100644 Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml index 90999e6..b7366cd 100644 --- a/.github/workflows/bump.yml +++ b/.github/workflows/bump.yml @@ -21,4 +21,3 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN }} WITH_V: false - DEFAULT_BUMP: patch diff --git a/Sources/OversizeUI/Controls/Avatar/Avatar.swift b/Sources/OversizeUI/Controls/Avatar/Avatar.swift index 60dd3ab..bc36704 100644 --- a/Sources/OversizeUI/Controls/Avatar/Avatar.swift +++ b/Sources/OversizeUI/Controls/Avatar/Avatar.swift @@ -78,6 +78,7 @@ public struct Avatar: View { if let avatar { avatar .resizable() + .scaledToFill() .frame(width: avatarSize, height: avatarSize) .clipShape(Circle()) .overlay(Circle().stroke(strokeColor, lineWidth: 2)) diff --git a/Sources/OversizeUI/Controls/DateField/DateField.swift b/Sources/OversizeUI/Controls/DateField/DateField.swift new file mode 100644 index 0000000..6889ded --- /dev/null +++ b/Sources/OversizeUI/Controls/DateField/DateField.swift @@ -0,0 +1,93 @@ +// +// Copyright © 2023 Alexander Romanov +// DateField.swift, created on 26.02.2023 +// + +import SwiftUI + +#if os(iOS) + @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + public struct DateField: View { + @Environment(\.theme) private var theme: ThemeSettings + @Environment(\.fieldLabelPosition) private var fieldPlaceholderPosition: FieldLabelPosition + @Binding private var selection: Date + @Binding private var optionalSelection: Date? + private let label: String + @State private var showModal = false + + let isOptionalSelection: Bool + + public init( + _ sheetTitle: String = "Date", + selection: Binding + ) { + label = sheetTitle + _selection = selection + _optionalSelection = .constant(nil) + isOptionalSelection = false + } + + public init( + _ label: String = "Date", + selection: Binding + ) { + self.label = label + _selection = .constant(Date()) + _optionalSelection = selection + isOptionalSelection = true + } + + public var body: some View { + VStack(alignment: .leading, spacing: .xSmall) { + if fieldPlaceholderPosition == .adjacent { + HStack { + Text(label) + .subheadline(.medium) + .foregroundColor(.onSurfaceHighEmphasis) + Spacer() + } + } + Button { + showModal.toggle() + } label: { + VStack(alignment: .leading, spacing: .xxxSmall) { + if fieldPlaceholderPosition == .overInput { + Text(label) + .font(.subheadline) + .fontWeight(.semibold) + .onSurfaceDisabledForegroundColor() + } + + HStack { + if isOptionalSelection, let optionalSelection { + Text(optionalSelection.formatted(date: .long, time: .shortened)) + } else if isOptionalSelection { + Text(label) + } else { + Text(selection.formatted(date: .long, time: .shortened)) + } + Spacer() + Icons.Base.calendar.outline + // Icon(.calendar, color: .onSurfaceHighEmphasis) + } + } + } + .buttonStyle(.field) + } + .sheet(isPresented: $showModal) { + if isOptionalSelection { + DatePickerSheet(title: label, selection: $optionalSelection) + .presentationDetents([.height(500)]) + .presentationDragIndicator(.hidden) + } else { + DatePickerSheet(title: label, selection: $selection) + .presentationDetents([.height(500)]) + .presentationDragIndicator(.hidden) + } + } + } + } +#endif diff --git a/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift b/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift new file mode 100644 index 0000000..4b0d78e --- /dev/null +++ b/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift @@ -0,0 +1,70 @@ +// +// Copyright © 2021 Alexander Romanov +// DatePickerSheet.swift, created on 11.12.2022 +// + +import SwiftUI + +public struct DatePickerSheet: View { + @Environment(\.screenSize) var screenSize + @Environment(\.dismiss) var dismiss + + @Binding private var selection: Date + @Binding private var optionalSelection: Date? + @State private var date: Date + + private let title: String + private var minimumDate: Date? + + public init(title: String, selection: Binding) { + self.title = title + _selection = selection + _date = State(wrappedValue: selection.wrappedValue) + _optionalSelection = .constant(nil) + } + + public init(title: String, selection: Binding) { + self.title = title + _date = State(wrappedValue: selection.wrappedValue ?? Date()) + _optionalSelection = selection + _selection = .constant(Date()) + } + + public var body: some View { + PageView(title) { + SectionView { + VStack { + if let minimumDate { + DatePicker("", selection: $date, in: minimumDate...) + .datePickerStyle(.graphical) + .labelsHidden() + } else { + DatePicker("", selection: $date) + .datePickerStyle(.graphical) + .labelsHidden() + } + } + .padding(.horizontal, .small) + .padding(.vertical, .xxxSmall) + } + .surfaceContentInsets(.zero) + } + .backgroundSecondary() + .leadingBar { + BarButton(.close) + } + .trailingBar { + BarButton(.accent("Done", action: { + selection = date + optionalSelection = date + dismiss() + })) + } + } + + public func datePickerMinimumDate(_ date: Date) -> DatePickerSheet { + var control = self + control.minimumDate = date + return control + } +} diff --git a/Sources/OversizeUI/Controls/PhoneField/PhoneField.swift b/Sources/OversizeUI/Controls/PhoneField/PhoneField.swift new file mode 100644 index 0000000..3fd5162 --- /dev/null +++ b/Sources/OversizeUI/Controls/PhoneField/PhoneField.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2023 Alexander Romanov +// PhoneField.swift, created on 05.03.2023 +// + +import SwiftUI + +#if os(iOS) + @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + public struct PhoneField: View { + @Binding private var phone: String + + @State private var textFieldHelper: FieldHelperStyle = .none + + public init(_ phone: Binding) { + _phone = phone + } + + public var body: some View { + TextField("+1 (000) 000 0000", text: $phone, onEditingChanged: { _ in + textFieldHelper = .none + }) {} + .keyboardType(.phonePad) + .textContentType(.nickname) + .fieldHelper(.constant("Invalid Phone"), style: $textFieldHelper) + } + } +#endif diff --git a/Sources/OversizeUI/Controls/PriceField/PriceField.swift b/Sources/OversizeUI/Controls/PriceField/PriceField.swift new file mode 100644 index 0000000..f8a4d22 --- /dev/null +++ b/Sources/OversizeUI/Controls/PriceField/PriceField.swift @@ -0,0 +1,37 @@ +// +// Copyright © 2023 Alexander Romanov +// PriceField.swift, created on 15.03.2023 +// + +import Foundation +import SwiftUI + +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +public struct PriceField: View { + @Binding private var amount: Decimal + private let currency: Locale.Currency + + public init(amount: Binding, currency: Locale.Currency) { + _amount = amount + self.currency = currency + } + + public var body: some View { + #if os(iOS) + TextField( + "0", + value: $amount, + format: .currency(code: currency.identifier) + ) + .keyboardType(.decimalPad) + .textFieldStyle(.default) + #else + TextField( + "0", + value: $amount, + format: .currency(code: currency.identifier) + ) + .textFieldStyle(.default) + #endif + } +} diff --git a/Sources/OversizeUI/Controls/URLField/URLField.swift b/Sources/OversizeUI/Controls/URLField/URLField.swift new file mode 100644 index 0000000..432d4af --- /dev/null +++ b/Sources/OversizeUI/Controls/URLField/URLField.swift @@ -0,0 +1,42 @@ +// +// Copyright © 2023 Alexander Romanov +// URLField.swift +// + +import SwiftUI + +#if os(iOS) + @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + public struct URLField: View { + @Binding private var url: URL? + @State private var urlString: String = "" + let title: String + + @State private var textFieldHelper: FieldHelperStyle = .none + + public init(_ title: String = "URL", url: Binding) { + self.title = title + _url = url + } + + public var body: some View { + if #available(iOS 16.0, *) { + TextField(title, value: $url, format: .url) + .keyboardType(.URL) + .textContentType(.URL) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } else { + TextField(title, text: $urlString) + .keyboardType(.URL) + .textContentType(.URL) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .fieldHelper(.constant("Invalid URL"), style: $textFieldHelper) + } + } + } +#endif diff --git a/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift b/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift new file mode 100644 index 0000000..189e6a3 --- /dev/null +++ b/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift @@ -0,0 +1,102 @@ +// +// Copyright © 2022 Alexander Romanov +// HalfSheet.swift, created on 30.12.2022 +// + +import SwiftUI +#if canImport(UIKit) + import UIKit +#endif + +public enum Detents: Hashable { + case large + case medium + case height(CGFloat) + + #if os(iOS) + @available(iOS 15, *) + public var uiViewDetents: UISheetPresentationController.Detent { + switch self { + case .large: + return .large() + case .medium: + return .medium() + case let .height(height): + return height > 560 ? .large() : .medium() + } + } + + @available(iOS 16, *) + func convertToSUI() -> PresentationDetent { + switch self { + case .large: + return PresentationDetent.large + case .medium: + return PresentationDetent.medium + case let .height(height): + return PresentationDetent.height(height) + } + } + #endif +} + +// swiftlint:disable line_length +#if os(iOS) + + public struct SheetModifier: ViewModifier { + public let detents: [Detents] + public func body(content: Content) -> some View { + SheetView(detents: detents) { + content + } + } + } + + public extension View { + @_disfavoredOverload + func presentationDetents(_ detents: [Detents]) -> some View { + Group { + if #available(iOS 16, *) { + let suiDetents: Set = Set(detents.compactMap { $0.convertToSUI() }) + self.presentationDetents(suiDetents) + } else { + modifier(SheetModifier(detents: detents)) + } + } + } + } + + public struct SheetView: UIViewControllerRepresentable { + private let content: Content + private let detents: [Detents] + + public init(detents: [Detents], @ViewBuilder content: () -> Content) { + self.content = content() + self.detents = detents + } + + public func makeUIViewController(context _: Context) -> SheetHostingController { + SheetHostingController(rootView: content, detents: detents.map { $0.uiViewDetents }) + } + + public func updateUIViewController(_: SheetHostingController, context _: Context) {} + } + + public final class SheetHostingController: UIHostingController { + var detents: [UISheetPresentationController.Detent] = [] + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let controller = sheetPresentationController { + controller.detents = detents + } + } + } + + public extension SheetHostingController { + convenience init(rootView: Content, detents: [UISheetPresentationController.Detent]) { + self.init(rootView: rootView) + self.detents = detents + } + } +#endif