Skip to content

Commit c74e97c

Browse files
authored
Add infrastructure for injecting media pickers (#201)
1 parent d244240 commit c74e97c

File tree

6 files changed

+183
-36
lines changed

6 files changed

+183
-36
lines changed

ios/Sources/GutenbergKit/Sources/EditorViewController.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
1414
public var configuration: EditorConfiguration
1515
private var _isEditorRendered = false
1616
private var _isEditorSetup = false
17+
private let mediaPicker: MediaPickerController?
1718
private let controller: GutenbergEditorController
1819
private let timestampInit = CFAbsoluteTimeGetCurrent()
1920

@@ -28,8 +29,13 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
2829
private let isWarmupMode: Bool
2930

3031
/// Initalizes the editor with the initial content (Gutenberg).
31-
public init(configuration: EditorConfiguration = .default, isWarmupMode: Bool = false) {
32+
public init(
33+
configuration: EditorConfiguration = .default,
34+
mediaPicker: MediaPickerController? = nil,
35+
isWarmupMode: Bool = false
36+
) {
3237
self.configuration = configuration
38+
self.mediaPicker = mediaPicker
3339
self.assetsLibrary = EditorAssetsLibrary(configuration: configuration)
3440
self.controller = GutenbergEditorController(configuration: configuration)
3541

@@ -240,11 +246,25 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
240246
// MARK: - Internal (Block Inserter)
241247

242248
private func showBlockInserter(blocks: [EditorBlock]) {
243-
present(UIHostingController(rootView: NavigationView {
244-
BlockInserterView(blocks: blocks) {
245-
print("did select:", $0)
246-
}
247-
}), animated: true)
249+
let context = MediaPickerPresentationContext()
250+
251+
let host = UIHostingController(rootView: NavigationStack {
252+
BlockInserterView(
253+
blocks: blocks,
254+
mediaPicker: mediaPicker,
255+
presentationContext: context,
256+
onBlockSelected: {
257+
print("insert blocks:", $0)
258+
},
259+
onMediaSelected: {
260+
print("insert media:", $0)
261+
}
262+
)
263+
})
264+
265+
context.viewController = host
266+
267+
present(host, animated: true)
248268
}
249269

250270
private func openMediaLibrary(_ config: OpenMediaLibraryAction) {

ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public protocol EditorViewControllerDelegate: AnyObject {
5252
/// - parameter dialogType: The type of modal dialog that closed (e.g., "block-inserter", "media-library").
5353
func editor(_ viewController: EditorViewController, didCloseModalDialog dialogType: String)
5454
}
55+
5556
#endif
5657

5758
public struct EditorState {
@@ -150,27 +151,3 @@ public struct OpenMediaLibraryAction: Codable {
150151
case multiple([Int])
151152
}
152153
}
153-
154-
public struct MediaInfo: Codable {
155-
public let id: Int32?
156-
public let url: String?
157-
public let type: String?
158-
public let title: String?
159-
public let caption: String?
160-
public let alt: String?
161-
public let metadata: [String: String]
162-
163-
private enum CodingKeys: String, CodingKey {
164-
case id, url, type, title, caption, alt, metadata
165-
}
166-
167-
public init(id: Int32?, url: String?, type: String?, caption: String? = nil, title: String? = nil, alt: String? = nil, metadata: [String: String] = [:]) {
168-
self.id = id
169-
self.url = url
170-
self.type = type
171-
self.caption = caption
172-
self.title = title
173-
self.alt = alt
174-
self.metadata = metadata
175-
}
176-
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
public struct MediaInfo: Codable {
4+
public let id: Int32?
5+
public let url: String?
6+
public let type: String?
7+
public let title: String?
8+
public let caption: String?
9+
public let alt: String?
10+
public let metadata: [String: String]
11+
12+
private enum CodingKeys: String, CodingKey {
13+
case id, url, type, title, caption, alt, metadata
14+
}
15+
16+
public init(id: Int32? = nil, url: String?, type: String?, caption: String? = nil, title: String? = nil, alt: String? = nil, metadata: [String: String] = [:]) {
17+
self.id = id
18+
self.url = url
19+
self.type = type
20+
self.caption = caption
21+
self.title = title
22+
self.alt = alt
23+
self.metadata = metadata
24+
}
25+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import UIKit
2+
3+
public struct MediaPickerAction: Identifiable {
4+
public let id: String
5+
public let title: String
6+
public let image: UIImage
7+
8+
init(id: String, title: String, image: UIImage) {
9+
self.id = id
10+
self.title = title
11+
self.image = image
12+
}
13+
}
14+
15+
public struct MediaPickerActionGroup: Identifiable {
16+
public let id: String
17+
public let actions: [MediaPickerAction]
18+
}
19+
20+
/// Configuration parameters for media picker behavior.
21+
public struct MediaPickerParameters {
22+
/// Filter that determines which types of media can be selected.
23+
public enum MediaFilter {
24+
case images
25+
case videos
26+
case all
27+
}
28+
29+
/// Optional filter to restrict the types of media that can be selected.
30+
public var filter: MediaFilter?
31+
32+
/// Whether users can select multiple media items at once.
33+
public var isMultipleSelectionEnabled: Bool
34+
35+
public init(filter: MediaFilter? = nil, isMultipleSelectionEnabled: Bool = false) {
36+
self.filter = filter
37+
self.isMultipleSelectionEnabled = isMultipleSelectionEnabled
38+
}
39+
}
40+
41+
public protocol MediaPickerController {
42+
/// Returns a grouped list of media picker actions for the given parameters.
43+
func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup]
44+
45+
/// Perform the action and return the selected media.
46+
func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo]
47+
}
48+
49+
final class MediaPickerPresentationContext {
50+
weak var viewController: UIViewController?
51+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import SwiftUI
2+
3+
struct MediaPickerMenu: View {
4+
let picker: MediaPickerController
5+
let context: MediaPickerPresentationContext
6+
var parameters = MediaPickerParameters()
7+
let onMediaSelected: ([MediaInfo]) -> Void
8+
9+
var body: some View {
10+
Menu {
11+
ForEach(picker.getActions(for: parameters)) { group in
12+
Section {
13+
ForEach(group.actions, content: makeButton)
14+
}
15+
}
16+
} label: {
17+
Image(systemName: "ellipsis")
18+
}
19+
}
20+
21+
private func makeButton(for action: MediaPickerAction) -> some View {
22+
Button {
23+
Task { @MainActor in
24+
if let viewController = context.viewController {
25+
let selection = await picker.perform(action, parameters: parameters, from: viewController)
26+
onMediaSelected(selection)
27+
}
28+
}
29+
} label: {
30+
Label {
31+
Text(action.title)
32+
} icon: {
33+
Image(uiImage: action.image)
34+
}
35+
}
36+
}
37+
}

ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,33 @@ import PhotosUI
33
import UIKit
44

55
struct BlockInserterView: View {
6+
let mediaPicker: MediaPickerController?
7+
let presentationContext: MediaPickerPresentationContext
68
let onBlockSelected: (EditorBlock) -> Void
9+
let onMediaSelected: ([MediaInfo]) -> Void
710

811
@StateObject private var viewModel: BlockInserterViewModel
912
@StateObject private var iconCache = BlockIconCache()
1013

1114
private let maxSelectionCount = 10
1215

1316
@Environment(\.dismiss) private var dismiss
14-
17+
1518
init(
1619
blocks: [EditorBlock],
20+
mediaPicker: MediaPickerController?,
21+
presentationContext: MediaPickerPresentationContext,
1722
onBlockSelected: @escaping (EditorBlock) -> Void,
23+
onMediaSelected: @escaping ([MediaInfo]) -> Void
1824
) {
25+
self.mediaPicker = mediaPicker
26+
self.presentationContext = presentationContext
1927
self.onBlockSelected = onBlockSelected
20-
let viewModel = BlockInserterViewModel(blocks: blocks)
21-
self._viewModel = StateObject(wrappedValue: viewModel)
28+
self.onMediaSelected = onMediaSelected
29+
30+
self._viewModel = StateObject(wrappedValue: BlockInserterViewModel(blocks: blocks))
2231
}
23-
32+
2433
var body: some View {
2534
content
2635
.background(Material.ultraThin)
@@ -57,13 +66,22 @@ struct BlockInserterView: View {
5766
}
5867
.tint(Color.primary)
5968
}
69+
70+
ToolbarItemGroup(placement: .topBarTrailing) {
71+
if let mediaPicker {
72+
MediaPickerMenu(picker: mediaPicker, context: presentationContext) {
73+
dismiss()
74+
onMediaSelected($0)
75+
}
76+
}
77+
}
6078
}
6179

6280
// MARK: - Actions
6381

64-
private func insertBlock(_ blockType: EditorBlock) {
82+
private func insertBlock(_ block: EditorBlock) {
6583
dismiss()
66-
onBlockSelected(blockType)
84+
onBlockSelected(block)
6785
}
6886
}
6987

@@ -74,10 +92,29 @@ struct BlockInserterView: View {
7492
NavigationStack {
7593
BlockInserterView(
7694
blocks: EditorBlock.mocks,
95+
mediaPicker: MockMediaPickerController(),
96+
presentationContext: MediaPickerPresentationContext(),
7797
onBlockSelected: {
7898
print("block selected: \($0.name)")
99+
},
100+
onMediaSelected: {
101+
print("media selected: \($0)")
79102
}
80103
)
81104
}
82105
}
106+
107+
struct MockMediaPickerController: MediaPickerController {
108+
func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup] {
109+
let group = MediaPickerActionGroup(id: "extra", actions: [
110+
MediaPickerAction(id: "files", title: "Files", image: UIImage(systemName: "folder")!)
111+
])
112+
return [group]
113+
}
114+
115+
func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo] {
116+
print("action selected:", action)
117+
return []
118+
}
119+
}
83120
#endif

0 commit comments

Comments
 (0)