Skip to content

Commit 1d5af5b

Browse files
feat: 🎸 HCPSDKFIORIUIKIT-2235 Attachment Part1 (SAP#983)
* feat: 🎸 HCPSDKFIORIUIKIT-2235 Attachment part1 draft Check in many files to prevent loss due to computer issues. With this check-ins, we have rough UI sckeleton. Current code allows customization of operations. and adpatable confiuration of SwiftUI QuickLookPreview or a custom preview allowing deletion. BREAKING CHANGE: 🧨 N/A ✅ Closes: 1 * refactor: 💡 API and Preview refactor APIs and implementation adjustment, preview examples BREAKING CHANGE: 🧨 N/A ✅ Closes: 0 * refactor: 💡 enable delete func and confirmation * feat: 🎸 adding control state and corresponding styles Started adding control state for components and related style updates. And update tests. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * feat: 🎸 introducing AttachmentProcessor protocol Adding a new protocol for processing attachemets, and misc updates. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * feat: 🎸 AttachmentProcessor protocol adoption and misc Plugin AttachmentProcessor and BaseAttachmentProcessor and misc improvements. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * refactor: 💡 adjust sourcery cfg and misc upates improve sourcery configuration and corresponding updates on implementations. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 date format adjustment per spec adjust date format pattern BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * Update BaseComponentProtocols.swift fix: missing closing curly brace. * Update AttachmentGroupStyle.fiori.swift fix: remove custom thumbnail, use custom style. * Update CompositeComponentProtocols.swift fix: remove func for custom thumbnail, use custom style instead. * chore: 🤖 adding generated files checkint generated supporting files. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 style issue with title, subtitle, footnote, and thumbnai fix style issue with title, subtitle, footnote, and LazyVGrid cache issue on thumbnail. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * refactor: 💡 allow single selection for file importer Update file import to allow selection one each time, adding default filter. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * refactor: 💡 BaseAttachmentProcessor and demo cases, and misc Update BaseAttachmentProcessor based demo case usage, and misc adjustments. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * refactor: 💡 rename processs to delegate, misc clean ups Use delegate naming convention instead of processor, misc. cleanups. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * chore: 🤖 code cleanup clean up code BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * docs: ✏️ adding docs, fix error processing, and update demo adding docs, fix error processing, and update demo BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 build warnings part1 fix build warnings part one. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 build warnings part2 fix build warnings part two. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 build warnings part3 fix build warnings part three. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 fix warnings part4 fix warnings part four BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 warnings fix build warnings BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 build warnings part5 fix warnings part five BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * chore: 🤖 moving attachment demos into a sub-group moving all attachment demos into a sub-group. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * chore: 🤖 update titles based on review feedback update titles based on review feedbacks BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * fix: 🐛 legacy preview delete button enabled in readonly mode BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * feat: 🎸 support camera (photo, video, and scanner) Adding support for take a photo, video, and use camera as scanner for pdf output. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 * chore: 🤖 update operation titles and icons using values from De update operation titles and icons using values provided by desiger. BREAKING CHANGE: 🧨 0 ✅ Closes: 0 --------- Co-authored-by: dyongxu <[email protected]>
1 parent e8440a1 commit 1d5af5b

File tree

72 files changed

+4179
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+4179
-2
lines changed

Apps/Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 112 additions & 0 deletions
Large diffs are not rendered by default.
54.6 KB
Loading
99 KB
Binary file not shown.
17.9 KB
Loading
1.7 MB
Binary file not shown.
8.58 KB
Binary file not shown.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import FioriSwiftUICore
2+
import SwiftUI
3+
4+
struct AttachmentDelegateExample: View {
5+
@State var groupOneAttachments: [URL]
6+
@State var groupTwoAttachments: [URL]
7+
@State var groupThreeAttachments: [URL]
8+
9+
@State var groupOneError: AttributedString?
10+
@State var groupTwoError: AttributedString?
11+
@State var groupThreeError: AttributedString?
12+
13+
let groupOneProeceeor: BasicAttachmentDelegate
14+
let groupTwoProeceeor: BasicAttachmentDelegate
15+
let groupThreeProeceeor: MyAttachmentDelegate
16+
17+
init() {
18+
var g1Attachments: [URL] = []
19+
self.groupOneProeceeor = BasicAttachmentDelegate(localFolderName: "groupOneAttachments") { folder in
20+
g1Attachments = BasicAttachmentDelegate.copy(
21+
attachments: [
22+
Bundle.main.url(forResource: "PDF advances in foundation - Landscape", withExtension: "pdf"),
23+
Bundle.main.url(forResource: "HTML File Example", withExtension: "html"),
24+
Bundle.main.url(forResource: "MD File Example", withExtension: "md")
25+
].compactMap { $0 },
26+
to: folder
27+
)
28+
}
29+
self.groupOneAttachments = g1Attachments
30+
self.groupOneError = nil
31+
32+
var g2Attachments: [URL] = []
33+
self.groupTwoProeceeor = BasicAttachmentDelegate(localFolderName: "groupTwoAttachments") { folder in
34+
g2Attachments = BasicAttachmentDelegate.copy(
35+
attachments: [
36+
Bundle.main.url(forResource: "Word File Example", withExtension: "docx"),
37+
Bundle.main.url(forResource: "Excel File Example", withExtension: "xlsx")
38+
].compactMap { $0 },
39+
to: folder
40+
)
41+
}
42+
43+
self.groupTwoAttachments = g2Attachments
44+
self.groupTwoError = nil
45+
46+
self.groupThreeAttachments = []
47+
self.groupThreeProeceeor = MyAttachmentDelegate()
48+
self.groupThreeError = nil
49+
}
50+
51+
var body: some View {
52+
ScrollView {
53+
VStack {
54+
AttachmentGroup(
55+
title: { Text("Finished/Readonly Attachments (\(self.groupOneAttachments.count))") },
56+
attachments: self.$groupOneAttachments,
57+
delegate: self.groupOneProeceeor,
58+
controlState: .readOnly,
59+
errorMessage: self.$groupOneError
60+
)
61+
62+
AttachmentGroup(
63+
title: { Text("WiP Attachments \(self.groupTwoAttachments.count) of 5") },
64+
attachments: self.$groupTwoAttachments,
65+
maxCount: 5,
66+
delegate: self.groupTwoProeceeor,
67+
errorMessage: self.$groupTwoError,
68+
operations: {
69+
AttachmentButtonImage()
70+
.operationsMenu {
71+
AttachmentMenuItems.photos(filter: [.images])
72+
AttachmentMenuItems.files(filter: [.pdf, .presentation])
73+
}
74+
}
75+
)
76+
77+
AttachmentGroup(
78+
title: { Text("Attachments w/ Custom Delegate") },
79+
attachments: self.$groupThreeAttachments,
80+
maxCount: 5,
81+
delegate: self.groupThreeProeceeor,
82+
errorMessage: self.$groupThreeError,
83+
operations: {
84+
AttachmentButtonImage()
85+
.operationsMenu {
86+
AttachmentMenuItems.photos(filter: [.images])
87+
AttachmentMenuItems.files(filter: [.pdf, .presentation])
88+
}
89+
}
90+
)
91+
}
92+
}
93+
}
94+
}
95+
96+
extension BasicAttachmentDelegate {
97+
static func copy(attachments: [URL], to folder: URL) -> [URL] {
98+
do {
99+
let mgr = FileManager.default
100+
if mgr.fileExists(atPath: folder.path) {
101+
if try mgr.contentsOfDirectory(atPath: folder.path).count > 0 {
102+
try mgr.removeItem(at: folder)
103+
try mgr.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil)
104+
}
105+
} else {
106+
try mgr.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil)
107+
}
108+
109+
try attachments.forEach {
110+
let attachmentURL = folder.appendingPathComponent($0.lastPathComponent)
111+
try FileManager.default.copyItem(at: $0, to: attachmentURL)
112+
}
113+
return try FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
114+
} catch {
115+
print("Error preparing attachments folder: \(error)")
116+
return []
117+
}
118+
}
119+
}
120+
121+
public class MyAttachmentDelegate: BasicAttachmentDelegate {
122+
public init() {
123+
super.init(localFolderName: "groupThreeAttachments") { folder in
124+
// you can play with commenting following code, which performs cleanup. Then, the second upload of the same file will cause an error.
125+
let _ = BasicAttachmentDelegate.copy(attachments: [], to: folder)
126+
}
127+
}
128+
129+
override public func getAttachmentNameAndExt(from url: URL, utTypeidentifier identifier: String) throws -> URL {
130+
let fileURL = try super.getAttachmentNameAndExt(from: url, utTypeidentifier: identifier)
131+
let ext = fileURL.pathExtension
132+
let name = fileURL.deletingPathExtension().lastPathComponent
133+
let folder = fileURL.deletingLastPathComponent()
134+
return folder.appendingPathComponent(name + "_processed").appendingPathExtension(ext)
135+
}
136+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import FioriSwiftUICore
2+
import SwiftUI
3+
4+
struct AttachmentExamples: View {
5+
var body: some View {
6+
List {
7+
Section(header: Text("Attachment")) {
8+
NavigationLink(
9+
destination: Sandbox(),
10+
label: {
11+
Text("Playground")
12+
}
13+
)
14+
15+
NavigationLink(
16+
destination: AttachmentGroupExample(),
17+
label: {
18+
Text("AttachmentGroup")
19+
}
20+
)
21+
22+
NavigationLink(
23+
destination: AttachmentGroupCustomExample(),
24+
label: {
25+
Text("AttachmentGroup -- Custom")
26+
}
27+
)
28+
29+
NavigationLink(
30+
destination: AttachmentDelegateExample(),
31+
label: {
32+
Text("Attachment Delegate Example")
33+
}
34+
)
35+
36+
NavigationLink(
37+
destination: AttachmentPreviewExample(),
38+
label: {
39+
Text("Preview Examples")
40+
}
41+
)
42+
}
43+
}
44+
.listStyle(.plain)
45+
.environment(\.defaultMinListRowHeight, 5)
46+
}
47+
}
48+
49+
#Preview {
50+
SimpleTimelineExample()
51+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import FioriSwiftUICore
2+
import PhotosUI
3+
import SwiftUI
4+
5+
struct AttachmentGroupCustomExample: View {
6+
@State var urls = [
7+
Bundle.main.url(forResource: "Text File Example", withExtension: "txt")!,
8+
Bundle.main.url(forResource: "XML File Example", withExtension: "xml")!,
9+
Bundle.main.url(forResource: "HTML File Example", withExtension: "html")!
10+
]
11+
12+
@State private var showFileImporter = false
13+
@State private var showPhotoPicker = false
14+
15+
@State var images = [UIImage]()
16+
@State var selectedPhotos = [PhotosPickerItem]()
17+
18+
@State private var errorMessage: AttributedString? = nil
19+
20+
@Environment(\.colorScheme) var colorScheme
21+
22+
var body: some View {
23+
ScrollView {
24+
VStack {
25+
AttachmentGroup(title: { Text("Attachments") }, attachments: self.$urls, errorMessage: self.$errorMessage) {
26+
Image(systemName: "plus")
27+
.foregroundColor(self.colorScheme == .dark ? .white : .blue)
28+
.font(.system(size: 24))
29+
.padding()
30+
.frame(width: 90, height: 90)
31+
.overlay(
32+
Circle()
33+
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5, 5]))
34+
.foregroundColor(self.colorScheme == .dark ? .white : .blue)
35+
)
36+
.operationsOverlay {
37+
self.overlayMenu
38+
}
39+
.frame(width: 109, height: 109)
40+
}
41+
.fileImporter(
42+
isPresented: self.$showFileImporter,
43+
allowedContentTypes: [.pdf, .text, .png, .plainText],
44+
allowsMultipleSelection: false
45+
) { result in
46+
switch result {
47+
case .success(let files):
48+
files.forEach { file in
49+
let gotAccess = file.startAccessingSecurityScopedResource()
50+
if !gotAccess { return }
51+
// app's logic goes here
52+
file.stopAccessingSecurityScopedResource()
53+
}
54+
case .failure(let error):
55+
print(error)
56+
}
57+
}
58+
.sheet(isPresented: self.$showPhotoPicker) {
59+
PhotosPicker(
60+
"Pick a photo",
61+
selection: self.$selectedPhotos,
62+
maxSelectionCount: 1, // make sure alwasy to use 1
63+
selectionBehavior: .ordered,
64+
matching: .images
65+
)
66+
.photosPickerStyle(.inline)
67+
}
68+
.onChange(of: self.selectedPhotos) { _, _ in
69+
// app's logic goes here
70+
}
71+
}
72+
}
73+
}
74+
75+
var overlayMenu: some View {
76+
func getOffsetX(index: Int, count: Int) -> CGFloat {
77+
let radius: CGFloat = 35
78+
let angle = CGFloat.pi * 1.5 / CGFloat(max(count - 1, 1))
79+
let elementAngle = CGFloat.pi * 1.25 + angle * CGFloat(index)
80+
return radius * cos(elementAngle)
81+
}
82+
83+
func getOffsetY(index: Int, count: Int) -> CGFloat {
84+
let radius: CGFloat = 35
85+
let angle = CGFloat.pi * 1.5 / CGFloat(max(count - 1, 1))
86+
let elementAngle = CGFloat.pi * 1.25 + angle * CGFloat(index)
87+
return radius * sin(elementAngle)
88+
}
89+
90+
return ZStack {
91+
Image(systemName: "camera")
92+
.font(.body)
93+
.foregroundStyle(.white)
94+
.padding(6)
95+
.background(.red)
96+
.clipShape(Circle())
97+
.offset(x: getOffsetX(index: 0, count: 6), y: getOffsetY(index: 0, count: 6))
98+
.onTapGesture {
99+
self.showPhotoPicker.toggle()
100+
}
101+
102+
Image(systemName: "photo")
103+
.font(.body)
104+
.foregroundStyle(.white)
105+
.padding(6)
106+
.background(.orange)
107+
.clipShape(Circle())
108+
.offset(x: getOffsetX(index: 1, count: 6), y: getOffsetY(index: 1, count: 6))
109+
.onTapGesture {
110+
self.showPhotoPicker.toggle()
111+
}
112+
113+
Image(systemName: "square.and.arrow.up")
114+
.font(.body)
115+
.foregroundStyle(.white)
116+
.padding(6)
117+
.background(.yellow)
118+
.clipShape(Circle())
119+
.offset(x: getOffsetX(index: 2, count: 6), y: getOffsetY(index: 2, count: 6))
120+
.onTapGesture {
121+
self.showFileImporter.toggle()
122+
}
123+
124+
Image(systemName: "book.closed.circle")
125+
.font(.body)
126+
.foregroundStyle(.white)
127+
.padding(6)
128+
.background(.green)
129+
.clipShape(Circle())
130+
.offset(x: getOffsetX(index: 3, count: 6), y: getOffsetY(index: 3, count: 6))
131+
.onTapGesture {}
132+
133+
Image(systemName: "doc.viewfinder")
134+
.font(.body)
135+
.foregroundStyle(.white)
136+
.padding(6)
137+
.background(.blue)
138+
.clipShape(Circle())
139+
.offset(x: getOffsetX(index: 4, count: 6), y: getOffsetY(index: 4, count: 6))
140+
.onTapGesture {}
141+
142+
// popping default file picker
143+
MyPhotoPickerButton()
144+
.offset(x: getOffsetX(index: 5, count: 6), y: getOffsetY(index: 5, count: 6))
145+
}
146+
}
147+
}
148+
149+
struct MyPhotoPickerButton: View {
150+
@Environment(AttachmentContext.self) var context
151+
152+
var body: some View {
153+
Button {
154+
self.context.showPhotosPicker.toggle()
155+
} label: {
156+
Image(systemName: "star.fill")
157+
.font(.body)
158+
.foregroundStyle(.white)
159+
.padding(6)
160+
.background(.yellow)
161+
.clipShape(Circle())
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)