diff --git a/Demo/Demo/Gravatar-SwiftUI-Demo/DemoProfileEditorView.swift b/Demo/Demo/Gravatar-SwiftUI-Demo/DemoProfileEditorView.swift index 34a9ed50..cc5875a8 100644 --- a/Demo/Demo/Gravatar-SwiftUI-Demo/DemoProfileEditorView.swift +++ b/Demo/Demo/Gravatar-SwiftUI-Demo/DemoProfileEditorView.swift @@ -33,8 +33,7 @@ struct DemoProfileEditorView: View { .gravatarQuickEditorSheet( isPresented: $isPresentingPicker, email: email, - scope: .avatarPicker, - contentLayout: contentLayoutOptions.contentLayout, + scope: .avatarPicker(.init(contentLayout: contentLayoutOptions.contentLayout)), onDismiss: { updateHasSession(with: email) } diff --git a/Demo/Demo/Gravatar-SwiftUI-Demo/QELayoutOptions.swift b/Demo/Demo/Gravatar-SwiftUI-Demo/QELayoutOptions.swift index 1948bea7..a3ca32c0 100644 --- a/Demo/Demo/Gravatar-SwiftUI-Demo/QELayoutOptions.swift +++ b/Demo/Demo/Gravatar-SwiftUI-Demo/QELayoutOptions.swift @@ -9,7 +9,7 @@ enum QELayoutOptions: String, Identifiable, CaseIterable { case verticalExpandablePrioritizeScrolling = "vertical - expandable - prioritize scrolling" case horizontal = "horizontal" - var contentLayout: AvatarPickerContentLayoutWithPresentation { + var contentLayout: AvatarPickerContentLayout { switch self { case .verticalLarge: .vertical(presentationStyle: .large) diff --git a/Demo/Demo/Gravatar-UIKit-Demo/DemoQuickEditorViewController.swift b/Demo/Demo/Gravatar-UIKit-Demo/DemoQuickEditorViewController.swift index 62677f32..ddeb4478 100644 --- a/Demo/Demo/Gravatar-UIKit-Demo/DemoQuickEditorViewController.swift +++ b/Demo/Demo/Gravatar-UIKit-Demo/DemoQuickEditorViewController.swift @@ -88,6 +88,25 @@ final class DemoQuickEditorViewController: UIViewController { present(sheet, animated: true) } + lazy var colorSchemeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Prefered color scheme:" + return label + }() + + lazy var schemeToggle: UISegmentedControl = { + let control = UISegmentedControl(items: [ + UIAction.init(title: "System") { _ in self.customColorScheme = .unspecified }, + UIAction.init(title: "Light") { _ in self.customColorScheme = .light }, + UIAction.init(title: "Dark") { _ in self.customColorScheme = .dark }, + ]) + control.selectedSegmentIndex = 0 + return control + }() + + var customColorScheme: UIUserInterfaceStyle = .unspecified + lazy var logoutButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false @@ -124,7 +143,15 @@ final class DemoQuickEditorViewController: UIViewController { }() lazy var rootStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [emailField, tokenField, layoutButton, logoutButton, showButton]) + let stackView = UIStackView(arrangedSubviews: [ + emailField, + tokenField, + colorSchemeLabel, + schemeToggle, + layoutButton, + logoutButton, + showButton + ]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = 12 @@ -149,11 +176,12 @@ final class DemoQuickEditorViewController: UIViewController { savedEmail = email let presenter = QuickEditorPresenter( email: Email(email), - scope: .avatarPicker, - avatarPickerConfiguration: AvatarPickerConfiguration(contentLayout: selectedLayout.contentLayout), + scope: .avatarPicker(AvatarPickerConfiguration(contentLayout: selectedLayout.contentLayout)), + configuration: .init( + interfaceStyle: customColorScheme + ), token: token ) - presenter.present(in: self, onDismiss: { [weak self] in self?.updateLogoutButton() }) diff --git a/Sources/GravatarUI/QuickEditor/QuickEditorConfiguration.swift b/Sources/GravatarUI/QuickEditor/QuickEditorConfiguration.swift new file mode 100644 index 00000000..00d28db8 --- /dev/null +++ b/Sources/GravatarUI/QuickEditor/QuickEditorConfiguration.swift @@ -0,0 +1,26 @@ +import UIKit + +public class QuickEditorConfiguration { + let interfaceStyle: UIUserInterfaceStyle + + static var `default`: QuickEditorConfiguration { .init() } + + public init( + interfaceStyle: UIUserInterfaceStyle? = nil + ) { + self.interfaceStyle = interfaceStyle ?? .unspecified + } +} + +/// Configuration which will be applied to the avatar picker screen. +public struct AvatarPickerConfiguration: Sendable { + let contentLayout: AvatarPickerContentLayout + + public init(contentLayout: AvatarPickerContentLayout) { + self.contentLayout = contentLayout + } + + static let `default` = AvatarPickerConfiguration( + contentLayout: .horizontal(presentationStyle: .intrinsicHeight) + ) +} diff --git a/Sources/GravatarUI/QuickEditor/QuickEditorViewController.swift b/Sources/GravatarUI/QuickEditor/QuickEditorViewController.swift index 3256c246..0ed649fc 100644 --- a/Sources/GravatarUI/QuickEditor/QuickEditorViewController.swift +++ b/Sources/GravatarUI/QuickEditor/QuickEditorViewController.swift @@ -1,24 +1,11 @@ import SwiftUI import UIKit -/// Configuration which will be applied to the avatar picker screen. -public struct AvatarPickerConfiguration: Sendable { - let contentLayout: AvatarPickerContentLayoutWithPresentation - - public init(contentLayout: AvatarPickerContentLayoutWithPresentation) { - self.contentLayout = contentLayout - } - - static let `default` = AvatarPickerConfiguration( - contentLayout: .horizontal(presentationStyle: .intrinsicHeight) - ) -} - final class QuickEditorViewController: UIViewController, ModalPresentationWithIntrinsicSize { let email: Email let scope: QuickEditorScope let token: String? - let avatarPickerConfiguration: AvatarPickerConfiguration + let configuration: QuickEditorConfiguration var onDismiss: (() -> Void)? = nil @@ -34,17 +21,20 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn var verticalSizeClass: UserInterfaceSizeClass? var sheetHeight: CGFloat = QEModalPresentationConstants.bottomSheetEstimatedHeight - var contentLayoutWithPresentation: AvatarPickerContentLayoutWithPresentation { - avatarPickerConfiguration.contentLayout + var contentLayoutWithPresentation: AvatarPickerContentLayout { + switch scope { + case .avatarPicker(let config): + config.contentLayout + } } private lazy var quickEditor: InnerHeightUIHostingController = .init(rootView: QuickEditor( email: email, - scope: scope, + scope: scope.scopeType, token: token, isPresented: isPresented, customImageEditor: nil as NoCustomEditorBlock?, - contentLayoutProvider: avatarPickerConfiguration.contentLayout + contentLayoutProvider: contentLayoutWithPresentation ), onHeightChange: { [weak self] newHeight in guard let self else { return } if self.shouldAcceptHeight(newHeight) { @@ -60,13 +50,13 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn init( email: Email, scope: QuickEditorScope, - avatarPickerConfiguration: AvatarPickerConfiguration? = nil, + configuration: QuickEditorConfiguration? = nil, token: String? = nil, onDismiss: (() -> Void)? = nil ) { self.email = email self.scope = scope - self.avatarPickerConfiguration = avatarPickerConfiguration ?? .default + self.configuration = configuration ?? .default self.token = token self.onDismiss = onDismiss super.init(nibName: nil, bundle: nil) @@ -105,12 +95,12 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn if let sheet = sheetPresentationController { sheet.animateChanges { sheet.detents = QEDetent.detents( - for: avatarPickerConfiguration.contentLayout, + for: contentLayoutWithPresentation, intrinsicHeight: sheetHeight, verticalSizeClass: verticalSizeClass ).map() } - sheet.prefersScrollingExpandsWhenScrolledToEdge = !avatarPickerConfiguration.contentLayout.prioritizeScrollOverResize + sheet.prefersScrollingExpandsWhenScrolledToEdge = !contentLayoutWithPresentation.prioritizeScrollOverResize } } } @@ -154,18 +144,18 @@ private class InnerHeightUIHostingController: UIHostingController { public struct QuickEditorPresenter { let email: Email let scope: QuickEditorScope - let avatarPickerConfiguration: AvatarPickerConfiguration + let configuration: QuickEditorConfiguration let token: String? public init( email: Email, scope: QuickEditorScope, - avatarPickerConfiguration: AvatarPickerConfiguration? = nil, + configuration: QuickEditorConfiguration? = nil, token: String? = nil ) { self.email = email self.scope = scope - self.avatarPickerConfiguration = avatarPickerConfiguration ?? .default + self.configuration = configuration ?? .default self.token = token } @@ -179,10 +169,12 @@ public struct QuickEditorPresenter { let quickEditor = QuickEditorViewController( email: email, scope: scope, - avatarPickerConfiguration: avatarPickerConfiguration, + configuration: configuration, token: token, onDismiss: onDismiss ) + + quickEditor.overrideUserInterfaceStyle = configuration.interfaceStyle quickEditor.onDismiss = onDismiss parent.present(quickEditor, animated: animated, completion: completion) } diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerContentLayout.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerContentLayout.swift index debb3053..0ee61db8 100644 --- a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerContentLayout.swift +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerContentLayout.swift @@ -24,7 +24,7 @@ public enum HorizontalContentPresentationStyle: String, Sendable, Equatable { } /// Content layout to use iOS 16.0 +. -public enum AvatarPickerContentLayoutWithPresentation: AvatarPickerContentLayoutProviding, Equatable { +public enum AvatarPickerContentLayout: AvatarPickerContentLayoutProviding, Equatable { /// Displays avatars in a vertcally scrolling grid with the given presentation style. See: ``VerticalContentPresentationStyle`` case vertical(presentationStyle: VerticalContentPresentationStyle = .large) @@ -34,7 +34,7 @@ public enum AvatarPickerContentLayoutWithPresentation: AvatarPickerContentLayout // MARK: AvatarPickerContentLayoutProviding - var contentLayout: AvatarPickerContentLayout { + var contentLayout: AvatarPickerContentLayoutType { switch self { case .horizontal: .horizontal @@ -54,8 +54,8 @@ public enum AvatarPickerContentLayoutWithPresentation: AvatarPickerContentLayout } /// Content layout to use pre iOS 16.0 where the system don't offer different presentation styles for SwiftUI. -/// Use ``AvatarPickerContentLayoutWithPresentation`` for iOS 16.0 +. -enum AvatarPickerContentLayout: String, CaseIterable, Identifiable, AvatarPickerContentLayoutProviding { +/// Use ``AvatarPickerContentLayout`` for iOS 16.0 +. +enum AvatarPickerContentLayoutType: String, CaseIterable, Identifiable, AvatarPickerContentLayoutProviding { var id: Self { self } /// Displays avatars in a vertcally scrolling grid. @@ -65,11 +65,11 @@ enum AvatarPickerContentLayout: String, CaseIterable, Identifiable, AvatarPicker // MARK: AvatarPickerContentLayoutProviding - var contentLayout: AvatarPickerContentLayout { self } + var contentLayout: AvatarPickerContentLayoutType { self } } -/// Internal type. This is an abstraction over `AvatarPickerContentLayout` and `AvatarPickerContentLayoutWithPresentation` +/// Internal type. This is an abstraction over `AvatarPickerContentLayoutType` and `AvatarPickerContentLayout` /// to use when all we are interested is to find out if the content is horizontial or vertical. protocol AvatarPickerContentLayoutProviding: Sendable { - var contentLayout: AvatarPickerContentLayout { get } + var contentLayout: AvatarPickerContentLayoutType { get } } diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift index 600c497e..8809ab08 100644 --- a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift @@ -17,7 +17,7 @@ struct AvatarPickerView: View { @State private var uploadError: FailedUploadInfo? @State private var isUploadErrorDialogPresented: Bool = false - var contentLayoutProvider: AvatarPickerContentLayoutProviding = AvatarPickerContentLayout.vertical + var contentLayoutProvider: AvatarPickerContentLayoutProviding = AvatarPickerContentLayoutType.vertical var customImageEditor: ImageEditorBlock? var tokenErrorHandler: (() -> Void)? @@ -506,7 +506,7 @@ private enum AvatarPicker { profileModel: PreviewModel() ) - return AvatarPickerView(model: model, isPresented: .constant(true), contentLayoutProvider: AvatarPickerContentLayout.horizontal) + return AvatarPickerView(model: model, isPresented: .constant(true), contentLayoutProvider: AvatarPickerContentLayoutType.horizontal) } #Preview("Empty elements") { diff --git a/Sources/GravatarUI/SwiftUI/AvatarPickerModalPresentationModifier.swift b/Sources/GravatarUI/SwiftUI/AvatarPickerModalPresentationModifier.swift index aab3ec05..72213ea8 100644 --- a/Sources/GravatarUI/SwiftUI/AvatarPickerModalPresentationModifier.swift +++ b/Sources/GravatarUI/SwiftUI/AvatarPickerModalPresentationModifier.swift @@ -24,9 +24,9 @@ struct AvatarPickerModalPresentationModifier: ViewModifier, Mod @State private var prioritizeScrollOverResize: Bool = false let onDismiss: (() -> Void)? let modalView: ModalView - var contentLayoutWithPresentation: AvatarPickerContentLayoutWithPresentation + var contentLayoutWithPresentation: AvatarPickerContentLayout - init(isPresented: Binding, onDismiss: (() -> Void)? = nil, modalView: ModalView, contentLayout: AvatarPickerContentLayoutWithPresentation) { + init(isPresented: Binding, onDismiss: (() -> Void)? = nil, modalView: ModalView, contentLayout: AvatarPickerContentLayout) { self._isPresented = isPresented self.isPresentedInner = isPresented.wrappedValue self.onDismiss = onDismiss @@ -89,7 +89,7 @@ struct AvatarPickerModalPresentationModifier: ViewModifier, Mod @MainActor protocol ModalPresentationWithIntrinsicSize { - var contentLayoutWithPresentation: AvatarPickerContentLayoutWithPresentation { get } + var contentLayoutWithPresentation: AvatarPickerContentLayout { get } var verticalSizeClass: UserInterfaceSizeClass? { get } } diff --git a/Sources/GravatarUI/SwiftUI/ProfileEditor/QuickEditor.swift b/Sources/GravatarUI/SwiftUI/ProfileEditor/QuickEditor.swift index 8848b781..0cd3effc 100644 --- a/Sources/GravatarUI/SwiftUI/ProfileEditor/QuickEditor.swift +++ b/Sources/GravatarUI/SwiftUI/ProfileEditor/QuickEditor.swift @@ -1,9 +1,21 @@ import SwiftUI -public enum QuickEditorScope: Sendable { +@available(iOS, deprecated: 16.0, renamed: "QuickEditorScope") +public enum QuickEditorScopeType: Sendable { case avatarPicker } +public enum QuickEditorScope: Sendable { + case avatarPicker(AvatarPickerConfiguration) + + var scopeType: QuickEditorScopeType { + switch self { + case .avatarPicker: + .avatarPicker + } + } +} + private enum QuickEditorConstants { static let title: String = "Gravatar" // defined here to avoid translations } @@ -13,7 +25,7 @@ struct QuickEditor: View { @Environment(\.oauthSession) private var oauthSession @State var hasSession: Bool = false - @State var scope: QuickEditorScope + @State var scope: QuickEditorScopeType @State var isAuthenticating: Bool = true @State var oauthError: OAuthError? @Binding var isPresented: Bool @@ -24,11 +36,11 @@ struct QuickEditor: View { init( email: Email, - scope: QuickEditorScope, + scope: QuickEditorScopeType, token: String? = nil, isPresented: Binding, customImageEditor: ImageEditorBlock? = nil, - contentLayoutProvider: AvatarPickerContentLayoutProviding = AvatarPickerContentLayout.vertical + contentLayoutProvider: AvatarPickerContentLayoutProviding = AvatarPickerContentLayoutType.vertical ) { self.email = email self.scope = scope @@ -196,6 +208,6 @@ extension QuickEditorConstants { email: .init(""), scope: .avatarPicker, isPresented: .constant(true), - contentLayoutProvider: AvatarPickerContentLayoutWithPresentation.vertical(presentationStyle: .large) + contentLayoutProvider: AvatarPickerContentLayout.vertical(presentationStyle: .large) ) } diff --git a/Sources/GravatarUI/SwiftUI/QEDetent.swift b/Sources/GravatarUI/SwiftUI/QEDetent.swift index fbc6f858..da456232 100644 --- a/Sources/GravatarUI/SwiftUI/QEDetent.swift +++ b/Sources/GravatarUI/SwiftUI/QEDetent.swift @@ -9,7 +9,7 @@ enum QEDetent { case height(CGFloat) static func detents( - for presentation: AvatarPickerContentLayoutWithPresentation, + for presentation: AvatarPickerContentLayout, intrinsicHeight: CGFloat, verticalSizeClass: UserInterfaceSizeClass? ) -> [QEDetent] { diff --git a/Sources/GravatarUI/SwiftUI/View+Additions.swift b/Sources/GravatarUI/SwiftUI/View+Additions.swift index ae330d98..f1e89975 100644 --- a/Sources/GravatarUI/SwiftUI/View+Additions.swift +++ b/Sources/GravatarUI/SwiftUI/View+Additions.swift @@ -11,7 +11,7 @@ extension View { ) } - @available(iOS, deprecated: 16.0, message: "Use the new method that takes in `AvatarPickerContentLayoutWithPresentation` for `contentLayout`.") + @available(iOS, deprecated: 16.0, message: "Use the new method that takes in `AvatarPickerContentLayout` for `contentLayout`.") public func avatarPickerSheet( isPresented: Binding, email: String, @@ -21,7 +21,7 @@ extension View { let avatarPickerView = AvatarPickerView( model: AvatarPickerViewModel(email: Email(email), authToken: authToken), isPresented: isPresented, - contentLayoutProvider: AvatarPickerContentLayout.vertical, + contentLayoutProvider: AvatarPickerContentLayoutType.vertical, customImageEditor: customImageEditor ) let navigationWrapped = NavigationView { avatarPickerView } @@ -33,7 +33,7 @@ extension View { isPresented: Binding, email: String, authToken: String, - contentLayout: AvatarPickerContentLayoutWithPresentation, + contentLayout: AvatarPickerContentLayout, customImageEditor: ImageEditorBlock? = nil as NoCustomEditorBlock? ) -> some View { let avatarPickerView = AvatarPickerView( @@ -56,11 +56,11 @@ extension View { .padding(.vertical, borderWidth) // to prevent borders from getting clipped } - @available(iOS, deprecated: 16.0, message: "Use the new method that takes in `AvatarPickerContentLayoutWithPresentation` for `contentLayout`.") + @available(iOS, deprecated: 16.0, message: "Use the new method that takes in `QuickEditorScope`.") public func gravatarQuickEditorSheet( isPresented: Binding, email: String, - scope: QuickEditorScope, + scope: QuickEditorScopeType, customImageEditor: ImageEditorBlock? = nil as NoCustomEditorBlock?, onDismiss: (() -> Void)? = nil ) -> some View { @@ -69,7 +69,7 @@ extension View { scope: scope, isPresented: isPresented, customImageEditor: customImageEditor, - contentLayoutProvider: AvatarPickerContentLayout.vertical + contentLayoutProvider: AvatarPickerContentLayoutType.vertical ) return modifier(ModalPresentationModifier(isPresented: isPresented, onDismiss: onDismiss, modalView: editor)) } @@ -80,22 +80,24 @@ extension View { email: String, scope: QuickEditorScope, customImageEditor: ImageEditorBlock? = nil as NoCustomEditorBlock?, - contentLayout: AvatarPickerContentLayoutWithPresentation, onDismiss: (() -> Void)? = nil ) -> some View { - let editor = QuickEditor( - email: .init(email), - scope: scope, - isPresented: isPresented, - customImageEditor: customImageEditor, - contentLayoutProvider: contentLayout - ) - return modifier(AvatarPickerModalPresentationModifier( - isPresented: isPresented, - onDismiss: onDismiss, - modalView: editor, - contentLayout: contentLayout - )) + switch scope { + case .avatarPicker(let config): + let editor = QuickEditor( + email: .init(email), + scope: scope.scopeType, + isPresented: isPresented, + customImageEditor: customImageEditor, + contentLayoutProvider: config.contentLayout + ) + return modifier(AvatarPickerModalPresentationModifier( + isPresented: isPresented, + onDismiss: onDismiss, + modalView: editor, + contentLayout: config.contentLayout + )) + } } func presentationContentInteraction(shouldPrioritizeScrolling: Bool) -> some View {