Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Demo/Demo/Gravatar-Demo/DemoQuickEditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,18 +284,18 @@ final class DemoQuickEditorViewController: UIViewController {

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 },
UIAction.init(title: "System") { [weak self] _ in self?.customColorScheme = .unspecified },
UIAction.init(title: "Light") { [weak self] _ in self?.customColorScheme = .light },
UIAction.init(title: "Dark") { [weak self] _ in self?.customColorScheme = .dark },
])
control.selectedSegmentIndex = 0
return control
}()

lazy var imageEditorToggle: UISegmentedControl = {
let control = UISegmentedControl(items: [
UIAction.init(title: "Default Image Editor") { _ in self.useCustomImageEditor = false },
UIAction.init(title: "Custom Image Editor") { _ in self.useCustomImageEditor = true },
UIAction.init(title: "Default Image Editor") { [weak self] _ in self?.useCustomImageEditor = false },
UIAction.init(title: "Custom Image Editor") { [weak self] _ in self?.useCustomImageEditor = true },
])
control.selectedSegmentIndex = 0
return control
Expand Down
25 changes: 23 additions & 2 deletions Demo/Demo/Gravatar-Demo/SwiftUI/DemoProfileEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ struct DemoProfileEditorView: View {
onDismiss: {
updateHasSession(with: email)
}
).environment(\.colorScheme, ColorScheme(selectedScheme) ?? .light)
)
.if(selectedScheme != .unspecified, transform: { content in
content.environment(\.colorScheme, ColorScheme(selectedScheme) ?? .light)
})
} else {
view
.gravatarQuickEditorSheet(
Expand All @@ -106,7 +109,10 @@ struct DemoProfileEditorView: View {
onDismiss: {
updateHasSession(with: email)
}
).environment(\.colorScheme, ColorScheme(selectedScheme) ?? .light)
)
.if(selectedScheme != .unspecified, transform: { content in
content.environment(\.colorScheme, ColorScheme(selectedScheme) ?? .light)
})
}
}
if hasSession {
Expand Down Expand Up @@ -265,3 +271,18 @@ struct DemoProfileEditorView: View {
#Preview {
DemoProfileEditorView()
}

extension View {
/// Applies the given transform if the given condition evaluates to `true`.
/// - Parameters:
/// - condition: The condition to evaluate.
/// - transform: The transform to apply to the source `View`.
/// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
if condition() {
transform(self)
} else {
self
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
ReferencedContainer = "container:Gravatar-Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand Down
20 changes: 12 additions & 8 deletions Sources/Gravatar/Network/Services/URLSessionHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,11 @@ struct URLSessionHTTPClient: HTTPClient {
private let urlSession: URLSessionProtocol

init(urlSession: URLSessionProtocol? = nil) {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Accept": "application/json",
"X-Platform": "ios",
"X-SDK-Version": BundleInfo.sdkVersion ?? "",
"X-Source": BundleInfo.appName ?? "",
]
self.urlSession = urlSession ?? URLSession(configuration: configuration)
self.urlSession = urlSession ?? URLSession(configuration: URLSessionConfiguration.default)
}

func data(with request: URLRequest) async throws -> (Data, HTTPURLResponse) {
let request = request.withHeaders()
let result: (data: Data, response: URLResponse)
do {
result = try await urlSession.data(for: request)
Expand All @@ -33,6 +27,7 @@ struct URLSessionHTTPClient: HTTPClient {
}

func uploadData(with request: URLRequest, data: Data) async throws -> (Data, HTTPURLResponse) {
let request = request.withHeaders()
let result: (data: Data, response: URLResponse)
do {
result = try await urlSession.upload(for: request, from: data)
Expand All @@ -49,6 +44,15 @@ extension URLRequest {
requestCopy.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return requestCopy
}

func withHeaders() -> URLRequest {
var request = self
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("ios", forHTTPHeaderField: "X-Platform")
request.addValue(BundleInfo.sdkVersion ?? "", forHTTPHeaderField: "X-SDK-Version")
request.addValue(BundleInfo.appName ?? "", forHTTPHeaderField: "X-Source")
return request
}
}

private func validatedHTTPResponse(_ response: URLResponse, data: Data) throws -> HTTPURLResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import UIKit

public class QuickEditorConfiguration {
public struct QuickEditorConfiguration {
let interfaceStyle: UIUserInterfaceStyle
let customImageEditorProvider: CustomImageEditorControllerProvider?

Expand Down
112 changes: 65 additions & 47 deletions Sources/GravatarUI/QuickEditor/QuickEditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,42 @@ import UIKit

public typealias CustomImageEditorControllerProvider = (UIImage, @escaping @Sendable (UIImage) -> Void) -> CustomImageEditorController

final class QuickEditorViewController: UIViewController, ModalPresentationWithIntrinsicSize {
private typealias CustomImageEditorProvider = ImageEditorBlock<CustomImageEditorControllerRepresentable>?

let email: Email
let scopeOption: QuickEditorScopeOption
let token: String?
let configuration: QuickEditorConfiguration
let updateHandler: ((QuickEditorUpdateType) -> Void)?
let onDismiss: (() -> Void)?
final class QuickEditorViewController<ImageEditor: ImageEditorView>: UIViewController,
ModalPresentationWithIntrinsicSize,
UISheetPresentationControllerDelegate
{
private let email: Email
private let token: String?
private let customImageEditorProvider: ImageEditorBlock<ImageEditor>?
private let updateHandler: ((QuickEditorUpdateType) -> Void)?
private let onDismiss: (() -> Void)?

private let unsavedChangesAlertPresentationModel = UnsavedChangesAlertPresentationModel()
private var sheetHeight: CGFloat = QEModalPresentationConstants.bottomSheetEstimatedHeight
private var currentPage: QuickEditorPage

private lazy var isPresented: Binding<Bool> = Binding {
true
} set: { isPresented in
} set: { [weak self] isPresented in
Task { @MainActor in
guard !isPresented else { return }
self.dismiss(animated: true)
self.onDismiss?()
self?.dismiss(animated: true)
self?.onDismiss?()
}
}

var verticalSizeClass: UserInterfaceSizeClass?
var sheetHeight: CGFloat = QEModalPresentationConstants.bottomSheetEstimatedHeight
var currentPage: QuickEditorPage

private lazy var rootView: QuickEditor = {
let provider: CustomImageEditorProvider = if let customProvider = configuration.customImageEditorProvider {
{ image, callback in
CustomImageEditorControllerRepresentable(
controllerProvider: customProvider,
inputImage: image,
editingDidFinish: callback
)
}
} else {
nil as ImageEditorBlock<CustomImageEditorControllerRepresentable>?
}
let scopeOption: QuickEditorScopeOption

return QuickEditor(
email: email,
scopeOption: scopeOption,
token: token,
isPresented: isPresented,
customImageEditor: provider,
updateHandler: updateHandler,
unsavedChangesAlertPresentationModel: unsavedChangesAlertPresentationModel
)
}()
private lazy var rootView = QuickEditor(
email: email,
scopeOption: scopeOption,
token: token,
isPresented: isPresented,
customImageEditor: customImageEditorProvider,
updateHandler: updateHandler,
unsavedChangesAlertPresentationModel: unsavedChangesAlertPresentationModel
)

private lazy var quickEditor: InnerHeightUIHostingController = .init(
rootView: rootView,
Expand All @@ -77,18 +64,19 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn
init(
email: Email,
scopeOption: QuickEditorScopeOption,
configuration: QuickEditorConfiguration? = nil,
customImageEditorProvider: ImageEditorBlock<ImageEditor>? = nil,
token: String? = nil,
onUpdate: ((QuickEditorUpdateType) -> Void)? = nil,
onDismiss: (() -> Void)? = nil
) {
self.email = email
self.scopeOption = scopeOption
self.configuration = configuration ?? .default

self.token = token
self.onDismiss = onDismiss
self.updateHandler = onUpdate
self.currentPage = scopeOption.initialPage
self.customImageEditorProvider = customImageEditorProvider
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -99,7 +87,6 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn

override func viewDidLoad() {
super.viewDidLoad()

quickEditor.willMove(toParent: self)
addChild(quickEditor)
view.addSubview(quickEditor.view)
Expand All @@ -123,7 +110,8 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn

func updateDetents() {
if let sheet = sheetPresentationController {
sheet.animateChanges {
sheet.animateChanges { [weak self] in
guard let self else { return }
sheet.detents = QEDetent.detents(
for: scopeOption,
intrinsicHeight: sheetHeight,
Expand All @@ -135,15 +123,17 @@ final class QuickEditorViewController: UIViewController, ModalPresentationWithIn
sheet.delegate = self
}
}
}

extension QuickEditorViewController: UISheetPresentationControllerDelegate {
func presentationControllerShouldDismiss(_: UIPresentationController) -> Bool {
if unsavedChangesAlertPresentationModel.hasUnsavedChanges {
unsavedChangesAlertPresentationModel.presentAlert = true
}
return !unsavedChangesAlertPresentationModel.hasUnsavedChanges
}

func presentationControllerDidDismiss(_: UIPresentationController) {
isPresented.wrappedValue = false
}
}

/// UIHostingController subclass which reads the InnerHeightPreferenceKey changes
Expand Down Expand Up @@ -203,7 +193,10 @@ private class InnerHeightUIHostingController: UIHostingController<AnyView> {
}

/// A struct responsible for presenting the Quick Editor from a UIKit context.
@MainActor
public struct QuickEditorPresenter {
private typealias CustomImageEditorProvider = ImageEditorBlock<CustomImageEditorControllerRepresentable>?

let email: Email
let scopeOption: QuickEditorScopeOption
let configuration: QuickEditorConfiguration
Expand Down Expand Up @@ -268,11 +261,24 @@ public struct QuickEditorPresenter {
onAvatarUpdated: (() -> Void)? = nil,
onDismiss: (() -> Void)? = nil
) {
let customImageEditorProvider: CustomImageEditorProvider = if let customProvider = configuration.customImageEditorProvider {
{ image, callback in
CustomImageEditorControllerRepresentable(
controllerProvider: customProvider,
inputImage: image,
editingDidFinish: callback
)
}
} else {
nil as ImageEditorBlock<CustomImageEditorControllerRepresentable>?
}

let quickEditor = QuickEditorViewController(
email: email,
scopeOption: scopeOption,
configuration: configuration,
customImageEditorProvider: customImageEditorProvider,
token: token,

onUpdate: { _ in
onAvatarUpdated?()
},
Expand All @@ -298,10 +304,22 @@ public struct QuickEditorPresenter {
onUpdate: ((QuickEditorUpdateType) -> Void)? = nil,
onDismiss: (() -> Void)? = nil
) {
let customImageEditorProvider: CustomImageEditorProvider = if let customProvider = configuration.customImageEditorProvider {
{ image, callback in
CustomImageEditorControllerRepresentable(
controllerProvider: customProvider,
inputImage: image,
editingDidFinish: callback
)
}
} else {
nil as ImageEditorBlock<CustomImageEditorControllerRepresentable>?
}

let quickEditor = QuickEditorViewController(
email: email,
scopeOption: scopeOption,
configuration: configuration,
customImageEditorProvider: customImageEditorProvider,
token: token,
onUpdate: onUpdate,
onDismiss: onDismiss
Expand All @@ -314,8 +332,8 @@ public struct QuickEditorPresenter {

/// A protocol defining a customizable image editor interface used in the Quick Editor flow.
///
/// This `UIViewController` subclass is presented after the user selects an image from their photo library and before it is uploaded to Gravatar. It provides an
/// opportunity to:
/// This `UIViewController` subclass is presented modally after the user selects an image from their photo library and before it is uploaded to Gravatar.
/// It provides an opportunity to:
/// - Enforce a square aspect ratio.
/// - Apply arbitrary, user-defined customizations to the image.
///
Expand Down
Loading