Skip to content

Commit

Permalink
Adding a configuration for the global quick editor experience (#450)
Browse files Browse the repository at this point in the history
  • Loading branch information
etoledom authored Oct 3, 2024
1 parent da9c11b commit b8b33eb
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 71 deletions.
3 changes: 1 addition & 2 deletions Demo/Demo/Gravatar-SwiftUI-Demo/DemoProfileEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion Demo/Demo/Gravatar-SwiftUI-Demo/QELayoutOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 32 additions & 4 deletions Demo/Demo/Gravatar-UIKit-Demo/DemoQuickEditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
})
Expand Down
26 changes: 26 additions & 0 deletions Sources/GravatarUI/QuickEditor/QuickEditorConfiguration.swift
Original file line number Diff line number Diff line change
@@ -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)
)
}
44 changes: 18 additions & 26 deletions Sources/GravatarUI/QuickEditor/QuickEditorViewController.swift
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -154,18 +144,18 @@ private class InnerHeightUIHostingController: UIHostingController<AnyView> {
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
}

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -34,7 +34,7 @@ public enum AvatarPickerContentLayoutWithPresentation: AvatarPickerContentLayout

// MARK: AvatarPickerContentLayoutProviding

var contentLayout: AvatarPickerContentLayout {
var contentLayout: AvatarPickerContentLayoutType {
switch self {
case .horizontal:
.horizontal
Expand All @@ -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.
Expand All @@ -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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct AvatarPickerView<ImageEditor: ImageEditorView>: 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<ImageEditor>?
var tokenErrorHandler: (() -> Void)?

Expand Down Expand Up @@ -506,7 +506,7 @@ private enum AvatarPicker {
profileModel: PreviewModel()
)

return AvatarPickerView<NoCustomEditor>(model: model, isPresented: .constant(true), contentLayoutProvider: AvatarPickerContentLayout.horizontal)
return AvatarPickerView<NoCustomEditor>(model: model, isPresented: .constant(true), contentLayoutProvider: AvatarPickerContentLayoutType.horizontal)
}

#Preview("Empty elements") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ struct AvatarPickerModalPresentationModifier<ModalView: View>: ViewModifier, Mod
@State private var prioritizeScrollOverResize: Bool = false
let onDismiss: (() -> Void)?
let modalView: ModalView
var contentLayoutWithPresentation: AvatarPickerContentLayoutWithPresentation
var contentLayoutWithPresentation: AvatarPickerContentLayout

init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, modalView: ModalView, contentLayout: AvatarPickerContentLayoutWithPresentation) {
init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, modalView: ModalView, contentLayout: AvatarPickerContentLayout) {
self._isPresented = isPresented
self.isPresentedInner = isPresented.wrappedValue
self.onDismiss = onDismiss
Expand Down Expand Up @@ -89,7 +89,7 @@ struct AvatarPickerModalPresentationModifier<ModalView: View>: ViewModifier, Mod

@MainActor
protocol ModalPresentationWithIntrinsicSize {
var contentLayoutWithPresentation: AvatarPickerContentLayoutWithPresentation { get }
var contentLayoutWithPresentation: AvatarPickerContentLayout { get }
var verticalSizeClass: UserInterfaceSizeClass? { get }
}

Expand Down
22 changes: 17 additions & 5 deletions Sources/GravatarUI/SwiftUI/ProfileEditor/QuickEditor.swift
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -13,7 +25,7 @@ struct QuickEditor<ImageEditor: ImageEditorView>: 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
Expand All @@ -24,11 +36,11 @@ struct QuickEditor<ImageEditor: ImageEditorView>: View {

init(
email: Email,
scope: QuickEditorScope,
scope: QuickEditorScopeType,
token: String? = nil,
isPresented: Binding<Bool>,
customImageEditor: ImageEditorBlock<ImageEditor>? = nil,
contentLayoutProvider: AvatarPickerContentLayoutProviding = AvatarPickerContentLayout.vertical
contentLayoutProvider: AvatarPickerContentLayoutProviding = AvatarPickerContentLayoutType.vertical
) {
self.email = email
self.scope = scope
Expand Down Expand Up @@ -196,6 +208,6 @@ extension QuickEditorConstants {
email: .init(""),
scope: .avatarPicker,
isPresented: .constant(true),
contentLayoutProvider: AvatarPickerContentLayoutWithPresentation.vertical(presentationStyle: .large)
contentLayoutProvider: AvatarPickerContentLayout.vertical(presentationStyle: .large)
)
}
2 changes: 1 addition & 1 deletion Sources/GravatarUI/SwiftUI/QEDetent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum QEDetent {
case height(CGFloat)

static func detents(
for presentation: AvatarPickerContentLayoutWithPresentation,
for presentation: AvatarPickerContentLayout,
intrinsicHeight: CGFloat,
verticalSizeClass: UserInterfaceSizeClass?
) -> [QEDetent] {
Expand Down
Loading

0 comments on commit b8b33eb

Please sign in to comment.