From 4cc8556f925148f0afb7ef8fa64bad78be050f43 Mon Sep 17 00:00:00 2001 From: fummicc1 Date: Thu, 21 Dec 2023 01:25:53 +0900 Subject: [PATCH 1/4] Use latest feature. use standard components. --- .../Csv2ImageApp.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../BackgroundColor.colorset/Contents.json | 9 +- .../Contents.json | 4 +- .../TextColor.colorset/Contents.json | 9 +- .../States/GenerateOutputState.swift | 2 + .../GenerateOutputModel.swift | 16 +- .../GenerateOutputView+macOS.swift | 189 ++++++++++-------- .../GeneratePreviewView.swift | 17 +- Sources/Csv2Img/Csv.swift | 11 +- Sources/Csv2Img/PDFMetadata.swift | 4 +- Sources/Csv2Img/PdfMaker.swift | 5 +- Sources/Csv2Img/PdfSize.swift | 2 +- 13 files changed, 156 insertions(+), 128 deletions(-) diff --git a/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.pbxproj b/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.pbxproj index d694c46..7d7d0f7 100644 --- a/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.pbxproj +++ b/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.pbxproj @@ -635,12 +635,12 @@ INFOPLIST_KEY_UIRequiresFullScreen = NO; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_PREPROCESS = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = dev.fummicc1.Csv2ImageApp_debug; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -680,12 +680,12 @@ INFOPLIST_KEY_UIRequiresFullScreen = NO; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_PREPROCESS = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = dev.fummicc1.Csv2ImageApp; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5ca3e0f..4285a54 100644 --- a/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Csv2ImageApp/Csv2ImageApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -20,12 +20,12 @@ } }, { - "package": "SwiftSyntax", + "package": "swift-syntax", "repositoryURL": "https://github.com/apple/swift-syntax", "state": { - "branch": "main", - "revision": "dfb8deb846e16d98e1d5b2261ed608121e096037", - "version": null + "branch": null, + "revision": "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version": "509.0.2" } } ] diff --git a/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/BackgroundColor.colorset/Contents.json b/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/BackgroundColor.colorset/Contents.json index aed8c0a..3c045e9 100644 --- a/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/BackgroundColor.colorset/Contents.json +++ b/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/BackgroundColor.colorset/Contents.json @@ -2,13 +2,8 @@ "colors" : [ { "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "77", - "green" : "77", - "red" : "77" - } + "platform" : "ios", + "reference" : "secondarySystemBackgroundColor" }, "idiom" : "universal" } diff --git a/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/SecondaryBackgroundColor.colorset/Contents.json b/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/SecondaryBackgroundColor.colorset/Contents.json index 2f0f228..9d32ca4 100644 --- a/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/SecondaryBackgroundColor.colorset/Contents.json +++ b/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/SecondaryBackgroundColor.colorset/Contents.json @@ -2,8 +2,8 @@ "colors" : [ { "color" : { - "platform" : "universal", - "reference" : "labelColor" + "platform" : "ios", + "reference" : "tertiarySystemBackgroundColor" }, "idiom" : "universal" } diff --git a/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/TextColor.colorset/Contents.json b/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/TextColor.colorset/Contents.json index ab9bd79..eb48633 100644 --- a/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/TextColor.colorset/Contents.json +++ b/Csv2ImageApp/Csv2ImageApp/Assets.xcassets/TextColor.colorset/Contents.json @@ -2,13 +2,8 @@ "colors" : [ { "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF0", - "green" : "0xF0", - "red" : "0xF0" - } + "platform" : "universal", + "reference" : "labelColor" }, "idiom" : "universal" } diff --git a/Csv2ImageApp/Csv2ImageApp/States/GenerateOutputState.swift b/Csv2ImageApp/Csv2ImageApp/States/GenerateOutputState.swift index b6dad5c..01ed6f7 100644 --- a/Csv2ImageApp/Csv2ImageApp/States/GenerateOutputState.swift +++ b/Csv2ImageApp/Csv2ImageApp/States/GenerateOutputState.swift @@ -20,6 +20,8 @@ struct GenerateOutputState: Hashable, Equatable { var encoding: String.Encoding var exportType: Csv.ExportType + var size: PdfSize = .a3 + var orientation: PdfSize.Orientation = .portrait var cgImage: CGImage? var pdfDocument: PDFDocument? diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift index c248ffe..7609a05 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift @@ -68,11 +68,17 @@ class GenerateOutputModel: ObservableObject { .combineLatest( _state.projectedValue .map(\.encoding) + .removeDuplicates(), + _state.projectedValue + .map(\.size) + .removeDuplicates(), + _state.projectedValue + .map(\.orientation) .removeDuplicates() ) .receive(on: queue) .share() - .sink { (_, _) in + .sink { (_, _, _, _) in Task { await self.updateCachedCsv() } @@ -83,6 +89,8 @@ class GenerateOutputModel: ObservableObject { func updateCachedCsv() async { let exportMode = await state.exportType let encoding = await state.encoding + let pdfSize = await state.size + let pdfOrientation = await state.orientation let url = await state.url let fileType = await state.fileType let csv: Csv? @@ -110,6 +118,12 @@ class GenerateOutputModel: ObservableObject { csvTask = Task { Task { do { + await csv.update( + pdfMetadata: .init( + size: pdfSize, + orientation: pdfOrientation + ) + ) let exportable = try await csv.generate(exportType: exportMode) if type(of: exportable.base) == PDFDocument.self { await self.update(keyPath: \.pdfDocument, value: (exportable.base as! PDFDocument)) diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+macOS.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+macOS.swift index abc5f6f..ced8df3 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+macOS.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+macOS.swift @@ -26,104 +26,127 @@ struct GenerateOutputView_macOS: View { @Environment(\.dismiss) var dismiss var body: some View { - GeometryReader { proxy in - ZStack { - if !model.state.isLoading { - VStack { - HStack { - CButton.labeled("Back") { - withAnimation { - backToPreviousPage = true - } + NavigationSplitView(sidebar: { + List { + Section("Export Type") { + Picker(selection: Binding(get: { + model.state.exportType + }, set: { exportType, _ in + model.update(keyPath: \.exportType, value: exportType) + })) { + CText("png").tag(Csv.ExportType.png) + CText("pdf").tag(Csv.ExportType.pdf) + } label: { + EmptyView() + } + .pickerStyle(.radioGroup) + } + Section("Encoding") { + let encodingInfo = model.state.encoding.description + Menu(encodingInfo) { + ForEach(availableEncodingType) { encoding in + Button { + model.update(keyPath: \.encoding, value: encoding) + } label: { + Text(encoding.description) } - Spacer() } - .padding() + } + .fixedSize() + } + Section("PDF Size") { + let encodingInfo = model.state.size.rawValue + Menu(encodingInfo) { + ForEach(PdfSize.allCases.indices, id: \.self) { index in + let size = PdfSize.allCases[index] + Button { + model.update(keyPath: \.size, value: size) + } label: { + Text(size.rawValue) + } + } + } + .fixedSize() + } + Section("PDF Orientation") { + let orientation = model.state.orientation.rawValue + Menu(orientation) { + ForEach(PdfSize.Orientation.allCases.indices, id: \.self) { index in + let orientation = PdfSize.Orientation.allCases[index] + Button { + model.update(keyPath: \.orientation, value: orientation) + } label: { + Text(orientation.rawValue) + } + } + } + .fixedSize() + } + } + VStack { + CButton.labeled("Save", role: .primary) { + model.save() + } + }.padding() + }, detail: { + if model.state.isLoading { + loadingContent + } else { + VStack(alignment: .center) { + GeometryReader { proxy in GeneratePreviewView( model: model, size: .constant( CGSize( - width: proxy.size.width * 0.7, - height: proxy.size.height * 0.7 + width: proxy.size.width, + height: proxy.size.height ) ) ) - .frame( - width: proxy.size.width * 0.7, - height: proxy.size.height * 0.7 - ) - Spacer() - HStack { - Picker(selection: Binding(get: { - model.state.exportType - }, set: { exportType, _ in - model.update(keyPath: \.exportType, value: exportType) - })) { - CText("png").tag(Csv.ExportType.png) - CText("pdf").tag(Csv.ExportType.pdf) - } label: { - CText("Export Type") - } - .padding() - .pickerStyle(.radioGroup) - .background(Asset.backgroundColor.swiftUIColor) - .padding() + } + } + } + }) + .background(Asset.lightAccentColor.swiftUIColor) - let encodingInfo = model.state.encoding.description - Menu("Encode Type: \(encodingInfo)") { - ForEach(availableEncodingType) { encoding in - Button { - model.update(keyPath: \.encoding, value: encoding) - } label: { - Text(encoding.description) - } + } - } - } + var loadingContent: some View { + VStack { + Spacer() + ProgressView(value: model.state.progress) { + CText("Loading...", font: .largeTitle) + } + .padding() + .progressViewStyle(.linear) + Spacer() + } + .background(Asset.lightAccentColor.swiftUIColor) + } - Spacer() - CButton.labeled("Save", role: .primary) { - model.save() - } - }.padding() - } - .background(Asset.lightAccentColor.swiftUIColor) - } else { - VStack { - Spacer() - ProgressView(value: model.state.progress) { - CText("Loading...", font: .largeTitle) - } - .padding() - .progressViewStyle(.linear) - Spacer() - } - .background(Asset.lightAccentColor.swiftUIColor) - } - VStack { - Spacer() - VStack { - CText(model.state.errorMessage ?? "", foregroundColor: .red) - .padding() - } - .fixedSize(horizontal: false, vertical: true) - .background(Asset.secondaryBackgroundColor.swiftUIColor) - .cornerRadius(12) + var errorContent: some View { + VStack { + Spacer() + VStack { + CText(model.state.errorMessage ?? "", foregroundColor: .red) .padding() - } - .opacity(model.state.errorMessage != nil ? 1 : 0) - .animation(.easeInOut, value: model.state.errorMessage) - .onChange(of: model.state.errorMessage, perform: { errorMessage in - if errorMessage != nil { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - withAnimation { - model.clearError() - } - } - } - }) } + .fixedSize(horizontal: false, vertical: true) + .background(Asset.secondaryBackgroundColor.swiftUIColor) + .cornerRadius(12) + .padding() } + .opacity(model.state.errorMessage != nil ? 1 : 0) + .animation(.easeInOut, value: model.state.errorMessage) + .onChange(of: model.state.errorMessage, perform: { errorMessage in + if errorMessage != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + withAnimation { + model.clearError() + } + } + } + }) } } diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift index 5e20dac..0d98387 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift @@ -40,18 +40,17 @@ struct GeneratePreviewView: View { var body: some View { Group { if let cgImage = model.state.cgImage, model.state.exportType == .png { - if let image = NSImage( + let image = NSImage( cgImage: cgImage, size: CGSize(width: cgImage.width, height: cgImage.height) - ) { - ScrollView(content: { - ScrollView(.horizontal, content: { - Image(nsImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - }) + ) + ScrollView(content: { + ScrollView(.horizontal, content: { + Image(nsImage: image) + .resizable() + .aspectRatio(contentMode: .fit) }) - } + }) } else if let document = model.state.pdfDocument, model.state.exportType == .pdf { PdfDocumentView(document: document, size: _size) } diff --git a/Sources/Csv2Img/Csv.swift b/Sources/Csv2Img/Csv.swift index a5e78c1..e8588db 100644 --- a/Sources/Csv2Img/Csv.swift +++ b/Sources/Csv2Img/Csv.swift @@ -140,9 +140,9 @@ public actor Csv { /// `pdfMetadata` stores pdf metadata which is used when ``Csv2Img.Csv.ExportType`` is `.png` private var pdfMetadata: PDFMetadata { didSet { - pdfMarker.set( - metadata: pdfMetadata - ) + pdfMarker.set( + metadata: pdfMetadata + ) } } @@ -556,8 +556,9 @@ extension Csv { } Task { do { - let doc: PDFDocument - if let pdfSize = maker.metadata.size, let orientation = maker.metadata.orientation { + let doc: PDFDocument + let orientation = maker.metadata.orientation + if let pdfSize = maker.metadata.size { doc = try maker.make( with: pdfSize, orientation: orientation, diff --git a/Sources/Csv2Img/PDFMetadata.swift b/Sources/Csv2Img/PDFMetadata.swift index acec5ad..5df96a1 100644 --- a/Sources/Csv2Img/PDFMetadata.swift +++ b/Sources/Csv2Img/PDFMetadata.swift @@ -12,13 +12,13 @@ public struct PDFMetadata { - specify output pdf size with ``PdfSize``. */ public var size: PdfSize? - public var orientation: PdfSize.Orientation? + public var orientation: PdfSize.Orientation public init( author: String? = nil, title: String? = nil, size: PdfSize? = nil, - orientation: PdfSize.Orientation? = nil + orientation: PdfSize.Orientation = .portrait ) { self.author = author self.title = title diff --git a/Sources/Csv2Img/PdfMaker.swift b/Sources/Csv2Img/PdfMaker.swift index d5532db..1271106 100644 --- a/Sources/Csv2Img/PdfMaker.swift +++ b/Sources/Csv2Img/PdfMaker.swift @@ -62,10 +62,10 @@ final class PdfMaker: PdfMakerType { Double ) -> Void ) throws -> PDFDocument { - return if let size = metadata.size, let orientation = metadata.orientation { + return if let size = metadata.size { try make( with: size, - orientation: orientation, + orientation: metadata.orientation, columns: columns, rows: rows, progress: progress @@ -536,7 +536,6 @@ final class PdfMaker: PdfMakerType { metadata: PDFMetadata ) { self.metadata = metadata - print(metadata) } } diff --git a/Sources/Csv2Img/PdfSize.swift b/Sources/Csv2Img/PdfSize.swift index eae0fdd..e460e25 100644 --- a/Sources/Csv2Img/PdfSize.swift +++ b/Sources/Csv2Img/PdfSize.swift @@ -15,7 +15,7 @@ public enum PdfSize: String, Codable, Equatable, CaseIterable, Sendable { case b4 case b5 - public enum Orientation: Codable, Equatable, CaseIterable, Sendable { + public enum Orientation: String, Codable, Equatable, CaseIterable, Sendable { case portrait case landscape } From 104bd299ca2f76e4cfae8a4b20d9bc2f52d2722f Mon Sep 17 00:00:00 2001 From: fummicc1 Date: Thu, 21 Dec 2023 02:27:00 +0900 Subject: [PATCH 2/4] tmp --- .../GenerateOutputModel.swift | 13 +- .../GenerateOutputView+iOS.swift | 157 ++++++++++++------ .../GeneratePreviewView.swift | 21 +-- 3 files changed, 118 insertions(+), 73 deletions(-) diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift index 7609a05..879ea5f 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift @@ -64,20 +64,17 @@ class GenerateOutputModel: ObservableObject { func onAppear() async { _state.projectedValue .map(\.exportType) - .removeDuplicates() + .share() .combineLatest( _state.projectedValue - .map(\.encoding) - .removeDuplicates(), + .map(\.encoding), _state.projectedValue - .map(\.size) - .removeDuplicates(), + .map(\.size), _state.projectedValue .map(\.orientation) - .removeDuplicates() ) .receive(on: queue) - .share() + .debounce(for: 1, scheduler: queue) .sink { (_, _, _, _) in Task { await self.updateCachedCsv() @@ -127,7 +124,7 @@ class GenerateOutputModel: ObservableObject { let exportable = try await csv.generate(exportType: exportMode) if type(of: exportable.base) == PDFDocument.self { await self.update(keyPath: \.pdfDocument, value: (exportable.base as! PDFDocument)) - } else { + } else {. await self.update(keyPath: \.cgImage, value: (exportable.base as! CGImage)) } } catch { diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift index ff3c271..4451665 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift @@ -16,22 +16,56 @@ struct GenerateOutputView_iOS: View { @Binding var backToPreviousPage: Bool @State private var succeedSavingOutput: Bool = false + private let availableEncodingType: [String.Encoding] = [ + .utf8, + .utf16, + .utf32, + .shiftJIS, + .ascii, + ] + var body: some View { - ZStack { - Rectangle() - .background(Asset.lightAccentColor.swiftUIColor) - .ignoresSafeArea() - Group { - VStack { - HStack { - CButton.labeled("Back") { - withAnimation { - backToPreviousPage = true - } - } - Spacer() + NavigationStack { + ZStack { + Rectangle() + .background(Asset.lightAccentColor.swiftUIColor) + .ignoresSafeArea() + loadedContent + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Save") { + succeedSavingOutput = model.save() } - .padding() + } + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Back") { + backToPreviousPage = true + } + } + } + .background(Asset.lightAccentColor.swiftUIColor) + .alert("Complete Saving!", isPresented: $succeedSavingOutput) { + CButton.labeled("Back") { + withAnimation { + backToPreviousPage = true + } + } + if let savedURL = model.savedURL, Application.shared.canOpenURL(savedURL) { + CButton.labeled("Open") { + Application.shared.open(savedURL) + } + } + } + } + } + + var loadedContent: some View { + VStack { + List { + Section("Export Type") { Picker(selection: Binding(get: { model.state.exportType }, set: { exportType in @@ -42,56 +76,75 @@ struct GenerateOutputView_iOS: View { CText("PNG") .tag(Csv.ExportType.png) } label: { - CText("Export Type") + EmptyView() } .pickerStyle(.segmented) - .padding() - GeometryReader { proxy in - VStack { - GeneratePreviewView( - model: model, - size: .constant( - CGSize( - width: proxy.size.width * 0.85, - height: proxy.size.height * 0.8 - ) - ) - ) - .padding() - Spacer() - HStack { - Spacer() - CButton.labeled("Save") { - succeedSavingOutput = model.save() - } + } + Section("Encoding") { + Menu(model.state.encoding.description) { + ForEach(availableEncodingType, id: \.self) { encoding in + Button { + model.update(keyPath: \.encoding, value: encoding) + } label: { + Text(encoding.description) } - .padding() } } + .fixedSize() } - .background(Asset.lightAccentColor.swiftUIColor) - } - if model.state.isLoading { - ProgressView { - CText("Loading...", font: .largeTitle) + Section("PDF Size") { + Menu(model.state.size.rawValue) { + ForEach(PdfSize.allCases.indices, id: \.self) { index in + let size = PdfSize.allCases[index] + Button { + model.update(keyPath: \.size, value: size) + } label: { + Text(size.rawValue) + } + } + } + .fixedSize() } - .padding() - .progressViewStyle(.linear) - } - } - .background(Asset.lightAccentColor.swiftUIColor) - .alert("Complete Saving!", isPresented: $succeedSavingOutput) { - CButton.labeled("Back") { - withAnimation { - backToPreviousPage = true + Section("PDF Orientation") { + Menu(model.state.orientation.rawValue) { + ForEach(PdfSize.Orientation.allCases.indices, id: \.self) { index in + let orientation = PdfSize.Orientation.allCases[index] + Button { + model.update(keyPath: \.orientation, value: orientation) + } label: { + Text(orientation.rawValue) + } + } + } + .fixedSize() } } - if let savedURL = model.savedURL, Application.shared.canOpenURL(savedURL) { - CButton.labeled("Open") { - Application.shared.open(savedURL) + .background(Asset.lightAccentColor.swiftUIColor) + + GeometryReader { proxy in + VStack(alignment: .center) { + GeneratePreviewView( + model: model, + size: .constant( + CGSize( + width: proxy.size.width, + height: proxy.size.height + ) + ) + ) } + } + .background(Asset.lightAccentColor.swiftUIColor) + } + } + + var loadingContent: some View { + ProgressView { + CText("Loading...", font: .largeTitle) } + .padding() + .progressViewStyle(.linear) } } #endif diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift index 0d98387..b7801c9 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift @@ -21,16 +21,15 @@ struct GeneratePreviewView: View { var body: some View { Group { if let cgImage = model.state.cgImage, model.state.exportType == .png { - if let image = UIImage(cgImage: cgImage) { - ScrollView { - ScrollView(.horizontal, content: { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - }) - } - .frame(width: size.width, height: size.height) + let image = UIImage(cgImage: cgImage) + ScrollView { + ScrollView(.horizontal, content: { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + }) } + .frame(width: size.width, height: size.height) } else if let document = model.state.pdfDocument, model.state.exportType == .pdf { PdfDocumentView(document: document, size: _size) } @@ -64,8 +63,4 @@ extension String.Encoding: Identifiable, Equatable { public var id: String { self.description } - - static func ==(lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id - } } From f30751eeb0859799317d860cb71bac0c411a2fee Mon Sep 17 00:00:00 2001 From: fummicc1 Date: Thu, 21 Dec 2023 02:47:10 +0900 Subject: [PATCH 3/4] tmp --- .../GenerateOutputModel.swift | 48 +- .../GenerateOutputView+iOS.swift | 8 +- .../GenerateOutputView.swift | 28 +- .../GeneratePreviewView.swift | 6 +- Sources/Csv2Img/Csv.swift | 1056 ++++++++--------- 5 files changed, 575 insertions(+), 571 deletions(-) diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift index 879ea5f..859f405 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputModel.swift @@ -62,25 +62,33 @@ class GenerateOutputModel: ObservableObject { @MainActor func onAppear() async { - _state.projectedValue - .map(\.exportType) - .share() - .combineLatest( - _state.projectedValue - .map(\.encoding), - _state.projectedValue - .map(\.size), - _state.projectedValue - .map(\.orientation) - ) - .receive(on: queue) - .debounce(for: 1, scheduler: queue) - .sink { (_, _, _, _) in - Task { - await self.updateCachedCsv() - } + Publishers.CombineLatest4( + _state.projectedValue + .map( + \.exportType + ).removeDuplicates(), + _state.projectedValue + .map( + \.encoding + ).removeDuplicates(), + _state.projectedValue + .map( + \.size + ).removeDuplicates(), + _state.projectedValue + .map( + \.orientation + ) + .removeDuplicates() + ) + .share() + .receive(on: queue) + .sink { (_, _, _, _) in + Task { + await self.updateCachedCsv() } - .store(in: &cancellables) + } + .store(in: &cancellables) } func updateCachedCsv() async { @@ -124,11 +132,11 @@ class GenerateOutputModel: ObservableObject { let exportable = try await csv.generate(exportType: exportMode) if type(of: exportable.base) == PDFDocument.self { await self.update(keyPath: \.pdfDocument, value: (exportable.base as! PDFDocument)) - } else {. + } else { await self.update(keyPath: \.cgImage, value: (exportable.base as! CGImage)) } } catch { - + print(error) } } Task { diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift index 4451665..28d06a6 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift @@ -26,12 +26,7 @@ struct GenerateOutputView_iOS: View { var body: some View { NavigationStack { - ZStack { - Rectangle() - .background(Asset.lightAccentColor.swiftUIColor) - .ignoresSafeArea() - loadedContent - } + loadedContent.id(model.state.isLoading) .toolbar { ToolbarItem(placement: .primaryAction) { Button("Save") { @@ -46,7 +41,6 @@ struct GenerateOutputView_iOS: View { } } } - .background(Asset.lightAccentColor.swiftUIColor) .alert("Complete Saving!", isPresented: $succeedSavingOutput) { CButton.labeled("Back") { withAnimation { diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView.swift index 09a83c3..d975979 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView.swift @@ -9,27 +9,29 @@ import SwiftUI struct GenerateOutputView: View { - + @StateObject var model: GenerateOutputModel @Binding var backToPreviousPage: Bool - + var body: some View { - Group { #if os(iOS) - GenerateOutputView_iOS( - model: model, - backToPreviousPage: _backToPreviousPage - ) -#elseif os(macOS) - GenerateOutputView_macOS( - model: model, - backToPreviousPage: _backToPreviousPage - ) -#endif + GenerateOutputView_iOS( + model: model, + backToPreviousPage: _backToPreviousPage + ) + .task { + await model.onAppear() } +#elseif os(macOS) + GenerateOutputView_macOS( + model: model, + backToPreviousPage: _backToPreviousPage + ) .task { await model.onAppear() } +#endif + } } diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift index b7801c9..50376b3 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GeneratePreviewView.swift @@ -59,8 +59,8 @@ struct GeneratePreviewView: View { } -extension String.Encoding: Identifiable, Equatable { - public var id: String { - self.description +extension String.Encoding: Identifiable { + public var id: UInt { + self.rawValue } } diff --git a/Sources/Csv2Img/Csv.swift b/Sources/Csv2Img/Csv.swift index e8588db..e91ea4f 100644 --- a/Sources/Csv2Img/Csv.swift +++ b/Sources/Csv2Img/Csv.swift @@ -67,16 +67,16 @@ public actor Csv { self.rows = rows self.exportType = exportType } - + private( set ) public var encoding: String.Encoding - + /// A flag whether ``Csv`` is loading contents or not public var isLoading: Bool { isLoadingSubject.value } - + /// A `Publisher` to send ``isLoading``. nonisolated public var isLoadingPublisher: AnyPublisher< Bool, @@ -84,7 +84,7 @@ public actor Csv { > { isLoadingSubject.eraseToAnyPublisher() } - + /// `CurrentValueSubject` to store ``isLoading``. private let isLoadingSubject: CurrentValueSubject< Bool, @@ -92,14 +92,14 @@ public actor Csv { > = .init( false ) - - + + /// progress stores current completeFraction of convert /// Value is in `0...1` with `Double` type public var progress: Double { progressSubject.value } - + /// A `Publisher` to send ``progress``. nonisolated public var progressPublisher: AnyPublisher< Double, @@ -107,7 +107,7 @@ public actor Csv { > { progressSubject.eraseToAnyPublisher() } - + /// `CurrentValueSubject` to store ``progress``. private let progressSubject: CurrentValueSubject< Double, @@ -115,25 +115,25 @@ public actor Csv { > = .init( 0 ) - + /// an separator applied to each row and column public var separator: String - + /// an array of ``Column`` public var columns: [Column] - + /// an array of row whose type is ``Row`. public var rows: [Row] - + /// ``ImageMarker`` has responsibility to generate png-image from csv private let imageMarker: ImageMaker - + /// ``PdfMaker`` has responsibility to generate pdf-image from csv private let pdfMarker: PdfMaker - + /// `rawString` is original String read from Resource (either Local or Network) public var rawString: String? - + /// `exportType` determines export type. Please choose ``ExportType.png`` or ``ExportType.pdf``. public var exportType: ExportType @@ -143,525 +143,525 @@ public actor Csv { pdfMarker.set( metadata: pdfMetadata ) - } - } - - /// ``maximumRowCount`` is the max number of Rows. this is fixed due to performance issue. - private let maximumRowCount: Int? = nil - - private let queue = DispatchQueue( - label: "dev.fummicc1.csv2img.csv-queue" - ) - - // MARK: Internal update functions - /// Internal method to update `Array` - func update( - rows: [Row] - ) { - self.rows = rows - } - /// Internal method to update `Array` - func update( - columns: [Column] - ) { - self.columns = columns - } - - func update( - columnStyles: [Column.Style] - ) { - columnStyles.enumerated().forEach { ( - i, - style - ) in - columns[i].style = style - } - } + } + } + + /// ``maximumRowCount`` is the max number of Rows. this is fixed due to performance issue. + private let maximumRowCount: Int? = nil + + private let queue = DispatchQueue( + label: "dev.fummicc1.csv2img.csv-queue" + ) + + // MARK: Internal update functions + /// Internal method to update `Array` + func update( + rows: [Row] + ) { + self.rows = rows + } + /// Internal method to update `Array` + func update( + columns: [Column] + ) { + self.columns = columns + } + + func update( + columnStyles: [Column.Style] + ) { + columnStyles.enumerated().forEach { ( + i, + style + ) in + columns[i].style = style + } + } } extension Csv { - /** - `ExportType` is a enum that expresses - */ - public enum ExportType: String, Hashable, CaseIterable { - /// `png` output - case png - /// `pdf` output (Work In Progress) - case pdf - - public var fileExtension: String { - self.rawValue - } - - public var utType: UTType { - switch self { - case .png: - return .png - case .pdf: - return .pdf - } - } - } + /** + `ExportType` is a enum that expresses + */ + public enum ExportType: String, Hashable, CaseIterable { + /// `png` output + case png + /// `pdf` output (Work In Progress) + case pdf + + public var fileExtension: String { + self.rawValue + } + + public var utType: UTType { + switch self { + case .png: + return .png + case .pdf: + return .pdf + } + } + } } extension Csv { - - /// Generate `Csv` from `String` data. - /// - /// You cloud call `Csv.loadFromString` if you can own raw-CSV data. - /// - /// ```swift - /// let rawCsv = """ - /// a,b,c - /// 1,2,3 - /// 4,5,6 - /// 7,8,9 - /// 10,11,12 - /// """ - /// let csv = Csv.loadFromString(rawCsv) - /// Output: - /// | a | b | c | - /// | 1 | 2 | 3 | - /// | 4 | 5 | 6 | - /// | 7 | 8 | 9 | - /// | 10 | 11 | 12 | - ///``` - /// - /// You cloud change separator by giving value to `separator` parameter. - /// - ///```swift - /// let dotSeparated = """ - /// a.b.c - /// 1.2.3 - /// 4.5.6 - /// 7.8.9 - /// """ - /// let csv = Csv.loadFromString(dotSeparated, separator: ".") - /// Output: - /// | a | b | c | - /// | 1 | 2 | 3 | - /// | 4 | 5 | 6 | - /// | 7 | 8 | 9 | - /// | 10 | 11 | 12 | - /// ``` - /// - /// If certain row-item is very long, you could trim it with `maxLength`-th length. - /// - ///```swift - /// let longCsv = """ - /// a.b.c - /// 1.2.33333333333333333333333333333333333333333 - /// 4.5.6 - /// 7.8.9 - /// """ - /// let csv = Csv.loadFromString(dotSeparated, separator: ".", maxLength: 7) - /// Output: - /// | a | b | c | - /// | 1 | 2 | 3333333 | - /// | 4 | 5 | 6 | - /// | 7 | 8 | 9 | - /// | 10 | 11 | 12 | - /// ``` - /// - /// - Parameters: - /// - str: Row String - /// - encoding: `String.Encoding?`. specify the encoding style used in generating String data. - /// - separator: Default separator in a row is `","`. You cloud change it by giving separator to `separator` parameter. - /// - maxLength: Default value is nil. if `maxLength` is not nil, every row-item length is limited by `maxLength`. - /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. - public static func loadFromString( - _ str: String, - encoding: String.Encoding = .utf8, - separator: String = ",", - maxLength: Int? = nil, - exportType: ExportType = .png - ) -> Csv { - var lines = str - .components( - separatedBy: CharacterSet( - charactersIn: "\r\n" - ) - ) - .filter({ - !$0.isEmpty - }) - var columns: [Csv.Column] = [] - var rows: [Row] = [] - - if lines.count == 1 { - let count = lines[0] - .split( - separator: Character( - separator - ), - omittingEmptySubsequences: false - ) - .count - let columns = ( - 0.. maxLength { - str = String( - item.prefix( - maxLength - ) - ) + "..." - } else { - str = item - } - return str - } - let row = Row( - index: i, - values: items - ) - rows.append( - row - ) - } - } - return Csv( - separator: separator, - rawString: str, - encoding: encoding, - columns: columns, - rows: rows, - exportType: .pdf - ) - } - - /// Generate `Csv` from network url (like `HTTPS`). - /// - /// - Parameters: - /// - url: Network url, commonly `HTTPS` schema. - /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. - /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. - /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. - public static func loadFromNetwork( - _ url: URL, - separator: String = ",", - encoding: String.Encoding = .utf8, - exportType: ExportType = .png - ) throws -> Csv { - let data = try Data( - contentsOf: url - ) - let str: String - if let _str = String( - data: data, - encoding: encoding - ) { - str = _str - } else { - throw Error.invalidDownloadResource( - url: url.absoluteString, - data: data - ) - } - return Csv.loadFromString( - str, - encoding: encoding, - separator: separator - ) - } - - /// Generate `Csv` from local disk url (like `file://Users/...`). - /// - /// - Parameters: - /// - file: Local disk url, commonly starts from `file://` schema. Relative-path method is not allowed, please specify by absolute-path method. - /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. - /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. - /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. - public static func loadFromDisk( - _ file: URL, - separator: String = ",", - encoding: String.Encoding = .utf8, - exportType: ExportType = .png - ) throws -> Csv { - // https://www.hackingwithswift.com/forums/swift/accessing-files-from-the-files-app/8203 - let canAccess = file.startAccessingSecurityScopedResource() - defer { - file.stopAccessingSecurityScopedResource() - } - if canAccess { - let data = try Data( - contentsOf: file - ) - let str: String - if let _str = String( - data: data, - encoding: encoding - ) { - str = _str - } else { - throw Error.invalidLocalResource( - url: file.absoluteString, - data: data, - encoding: encoding - ) - } - return Csv.loadFromString( - str, - encoding: encoding, - separator: separator - ) - } - throw Error.cannotAccessFile( - url: file.absoluteString - ) - } - - /** - Generate Output (file-type is determined by `exportType` parameter) - - Parameters: - - fontSize: Determine the fontsize of characters in output-table image. - - exportType:Determine file-extension. type: ``ExportType``. default value: ``ExportType.png``. If you use too big image size, I recommend use `.pdf` instead of `.png`. - - Note: - `fontSize` determines the size of output image and it can be as large as you want. Please consider the case that output image is too large to open image. Although output image becomes large, it is recommended to set fontSize amply enough (maybe larger than `12pt`) to see image clearly. - - Returns: ``CsvExportable``. (either ``CGImage`` or ``PdfDocument``). - - Throws: Throws ``Csv.Error``. - */ - public func generate( - fontSize: Double? = nil, - exportType: ExportType = .png, - styles: [Csv.Column.Style]? = nil - ) async throws -> AnyCsvExportable { - if isLoading { - throw Csv.Error.workInProgress - } - isLoadingSubject.value = true - progressSubject.value = 0 - defer { - isLoadingSubject.value = false - } - if columns.isEmpty || rows.isEmpty { - throw Csv.Error.emptyData - } - self.exportType = exportType - if let styles { - update( - columnStyles: styles - ) - } - var maker: Any? - switch exportType { - case .png: - maker = self.imageMarker - case .pdf: - maker = self.pdfMarker - } - if let maker = maker as? ImageMaker { - if let fontSize = fontSize { - maker.set( - fontSize: fontSize - ) - } - let exportable: any CsvExportable = try await withCheckedThrowingContinuation { continuation in - queue.async { [weak self] in - guard let self = self else { - continuation.resume( - throwing: Csv.Error.underlying( - nil - ) - ) - return - } - Task { - do { - let img = try maker.make( - columns: await self.columns, - rows: await self.rows - ) { progress in - self.progressSubject.value = progress - } - continuation.resume( - returning: img - ) - } catch { - continuation.resume( - throwing: Csv.Error.underlying( - error - ) - ) - } - } - } - } - return AnyCsvExportable( - exportable - ) - } else if let maker = maker as? PdfMaker { - if let fontSize = fontSize { - maker.set( - fontSize: fontSize - ) - } - let exportable: PDFDocument = try await withCheckedThrowingContinuation { continuation in - queue.async { [weak self] in - guard let self = self else { - continuation.resume( - throwing: Csv.Error.underlying( - nil - ) - ) - return - } - Task { - do { - let doc: PDFDocument - let orientation = maker.metadata.orientation - if let pdfSize = maker.metadata.size { - doc = try maker.make( - with: pdfSize, - orientation: orientation, - columns: await self.columns, - rows: await self.rows - ) { progress in - self.progressSubject.value = progress - } - } else { - doc = try maker.make( - columns: await self.columns, - rows: await self.rows - ) { progress in - self.progressSubject.value = progress - } - } - continuation.resume( - returning: doc - ) - } catch { - continuation.resume( - throwing: Csv.Error.underlying( - error - ) - ) - } - } - } - } - return AnyCsvExportable( - exportable - ) - } - throw Error.invalidExportType( - exportType - ) - } - - public func generate( - fontSize: Double? = nil, - exportType: ExportType = .png, - style: Csv.Column.Style - ) async throws -> AnyCsvExportable { - try await self.generate( - fontSize: fontSize, - exportType: exportType, - styles: columns.map { - _ in - style - } - ) - } - - /** - - parameters: - - to url: local file path where [png, pdf] image will be saved. - - Returns: If saving csv image to file, returns `true`. Otherwise, return `False`. - */ - public func write( - to url: URL - ) -> Data? { - let data: Data? - if exportType == .png { - data = imageMarker.latestOutput?.convertToData() - } else if exportType == .pdf { - pdfMarker.latestOutput?.write( - to: url - ) - return pdfMarker.latestOutput?.dataRepresentation() - } else { - data = nil - } - guard let data = data else { - return nil - } - do { - if !FileManager.default.fileExists( - atPath: url.absoluteString - ) { - FileManager.default.createFile( - atPath: url.absoluteString, - contents: data - ) - } else { - try data.write( - to: url - ) - } - return data - } catch { - print( - error - ) - return nil - } - } - - /** - - set ``PdfMetadata`` - */ - public func update( - pdfMetadata: PDFMetadata - ) { + + /// Generate `Csv` from `String` data. + /// + /// You cloud call `Csv.loadFromString` if you can own raw-CSV data. + /// + /// ```swift + /// let rawCsv = """ + /// a,b,c + /// 1,2,3 + /// 4,5,6 + /// 7,8,9 + /// 10,11,12 + /// """ + /// let csv = Csv.loadFromString(rawCsv) + /// Output: + /// | a | b | c | + /// | 1 | 2 | 3 | + /// | 4 | 5 | 6 | + /// | 7 | 8 | 9 | + /// | 10 | 11 | 12 | + ///``` + /// + /// You cloud change separator by giving value to `separator` parameter. + /// + ///```swift + /// let dotSeparated = """ + /// a.b.c + /// 1.2.3 + /// 4.5.6 + /// 7.8.9 + /// """ + /// let csv = Csv.loadFromString(dotSeparated, separator: ".") + /// Output: + /// | a | b | c | + /// | 1 | 2 | 3 | + /// | 4 | 5 | 6 | + /// | 7 | 8 | 9 | + /// | 10 | 11 | 12 | + /// ``` + /// + /// If certain row-item is very long, you could trim it with `maxLength`-th length. + /// + ///```swift + /// let longCsv = """ + /// a.b.c + /// 1.2.33333333333333333333333333333333333333333 + /// 4.5.6 + /// 7.8.9 + /// """ + /// let csv = Csv.loadFromString(dotSeparated, separator: ".", maxLength: 7) + /// Output: + /// | a | b | c | + /// | 1 | 2 | 3333333 | + /// | 4 | 5 | 6 | + /// | 7 | 8 | 9 | + /// | 10 | 11 | 12 | + /// ``` + /// + /// - Parameters: + /// - str: Row String + /// - encoding: `String.Encoding?`. specify the encoding style used in generating String data. + /// - separator: Default separator in a row is `","`. You cloud change it by giving separator to `separator` parameter. + /// - maxLength: Default value is nil. if `maxLength` is not nil, every row-item length is limited by `maxLength`. + /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. + public static func loadFromString( + _ str: String, + encoding: String.Encoding = .utf8, + separator: String = ",", + maxLength: Int? = nil, + exportType: ExportType = .png + ) -> Csv { + var lines = str + .components( + separatedBy: CharacterSet( + charactersIn: "\r\n" + ) + ) + .filter({ + !$0.isEmpty + }) + var columns: [Csv.Column] = [] + var rows: [Row] = [] + + if lines.count == 1 { + let count = lines[0] + .split( + separator: Character( + separator + ), + omittingEmptySubsequences: false + ) + .count + let columns = ( + 0.. maxLength { + str = String( + item.prefix( + maxLength + ) + ) + "..." + } else { + str = item + } + return str + } + let row = Row( + index: i, + values: items + ) + rows.append( + row + ) + } + } + return Csv( + separator: separator, + rawString: str, + encoding: encoding, + columns: columns, + rows: rows, + exportType: .pdf + ) + } + + /// Generate `Csv` from network url (like `HTTPS`). + /// + /// - Parameters: + /// - url: Network url, commonly `HTTPS` schema. + /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. + /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. + /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. + public static func loadFromNetwork( + _ url: URL, + separator: String = ",", + encoding: String.Encoding = .utf8, + exportType: ExportType = .png + ) throws -> Csv { + let data = try Data( + contentsOf: url + ) + let str: String + if let _str = String( + data: data, + encoding: encoding + ) { + str = _str + } else { + throw Error.invalidDownloadResource( + url: url.absoluteString, + data: data + ) + } + return Csv.loadFromString( + str, + encoding: encoding, + separator: separator + ) + } + + /// Generate `Csv` from local disk url (like `file://Users/...`). + /// + /// - Parameters: + /// - file: Local disk url, commonly starts from `file://` schema. Relative-path method is not allowed, please specify by absolute-path method. + /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. + /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. + /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. + public static func loadFromDisk( + _ file: URL, + separator: String = ",", + encoding: String.Encoding = .utf8, + exportType: ExportType = .png + ) throws -> Csv { + // https://www.hackingwithswift.com/forums/swift/accessing-files-from-the-files-app/8203 + let canAccess = file.startAccessingSecurityScopedResource() + defer { + file.stopAccessingSecurityScopedResource() + } + if canAccess { + let data = try Data( + contentsOf: file + ) + let str: String + if let _str = String( + data: data, + encoding: encoding + ) { + str = _str + } else { + throw Error.invalidLocalResource( + url: file.absoluteString, + data: data, + encoding: encoding + ) + } + return Csv.loadFromString( + str, + encoding: encoding, + separator: separator + ) + } + throw Error.cannotAccessFile( + url: file.absoluteString + ) + } + + /** + Generate Output (file-type is determined by `exportType` parameter) + - Parameters: + - fontSize: Determine the fontsize of characters in output-table image. + - exportType:Determine file-extension. type: ``ExportType``. default value: ``ExportType.png``. If you use too big image size, I recommend use `.pdf` instead of `.png`. + - Note: + `fontSize` determines the size of output image and it can be as large as you want. Please consider the case that output image is too large to open image. Although output image becomes large, it is recommended to set fontSize amply enough (maybe larger than `12pt`) to see image clearly. + - Returns: ``CsvExportable``. (either ``CGImage`` or ``PdfDocument``). + - Throws: Throws ``Csv.Error``. + */ + public func generate( + fontSize: Double? = nil, + exportType: ExportType = .png, + styles: [Csv.Column.Style]? = nil + ) async throws -> AnyCsvExportable { + if isLoading { + throw Csv.Error.workInProgress + } + isLoadingSubject.value = true + progressSubject.value = 0 + defer { + isLoadingSubject.value = false + } + if columns.isEmpty || rows.isEmpty { + throw Csv.Error.emptyData + } + self.exportType = exportType + if let styles { + update( + columnStyles: styles + ) + } + var maker: Any? + switch exportType { + case .png: + maker = self.imageMarker + case .pdf: + maker = self.pdfMarker + } + if let maker = maker as? ImageMaker { + if let fontSize = fontSize { + maker.set( + fontSize: fontSize + ) + } + let exportable: any CsvExportable = try await withCheckedThrowingContinuation { continuation in + queue.async { [weak self] in + guard let self = self else { + continuation.resume( + throwing: Csv.Error.underlying( + nil + ) + ) + return + } + Task { + do { + let img = try maker.make( + columns: await self.columns, + rows: await self.rows + ) { progress in + self.progressSubject.value = progress + } + continuation.resume( + returning: img + ) + } catch { + continuation.resume( + throwing: Csv.Error.underlying( + error + ) + ) + } + } + } + } + return AnyCsvExportable( + exportable + ) + } else if let maker = maker as? PdfMaker { + if let fontSize = fontSize { + maker.set( + fontSize: fontSize + ) + } + let exportable: PDFDocument = try await withCheckedThrowingContinuation { continuation in + queue.async { [weak self] in + guard let self = self else { + continuation.resume( + throwing: Csv.Error.underlying( + nil + ) + ) + return + } + Task { + do { + let doc: PDFDocument + let orientation = maker.metadata.orientation + if let pdfSize = maker.metadata.size { + doc = try maker.make( + with: pdfSize, + orientation: orientation, + columns: await self.columns, + rows: await self.rows + ) { progress in + self.progressSubject.value = progress + } + } else { + doc = try maker.make( + columns: await self.columns, + rows: await self.rows + ) { progress in + self.progressSubject.value = progress + } + } + continuation.resume( + returning: doc + ) + } catch { + continuation.resume( + throwing: Csv.Error.underlying( + error + ) + ) + } + } + } + } + return AnyCsvExportable( + exportable + ) + } + throw Error.invalidExportType( + exportType + ) + } + + public func generate( + fontSize: Double? = nil, + exportType: ExportType = .png, + style: Csv.Column.Style + ) async throws -> AnyCsvExportable { + try await self.generate( + fontSize: fontSize, + exportType: exportType, + styles: columns.map { + _ in + style + } + ) + } + + /** + - parameters: + - to url: local file path where [png, pdf] image will be saved. + - Returns: If saving csv image to file, returns `true`. Otherwise, return `False`. + */ + public func write( + to url: URL + ) -> Data? { + let data: Data? + if exportType == .png { + data = imageMarker.latestOutput?.convertToData() + } else if exportType == .pdf { + pdfMarker.latestOutput?.write( + to: url + ) + return pdfMarker.latestOutput?.dataRepresentation() + } else { + data = nil + } + guard let data = data else { + return nil + } + do { + if !FileManager.default.fileExists( + atPath: url.absoluteString + ) { + FileManager.default.createFile( + atPath: url.absoluteString, + contents: data + ) + } else { + try data.write( + to: url + ) + } + return data + } catch { + print( + error + ) + return nil + } + } + + /** + - set ``PdfMetadata`` + */ + public func update( + pdfMetadata: PDFMetadata + ) { self.pdfMetadata = pdfMetadata } } From 08a566d31889edc8248f41d1e9a55a939bd5cfe8 Mon Sep 17 00:00:00 2001 From: fummicc1 Date: Thu, 21 Dec 2023 02:57:01 +0900 Subject: [PATCH 4/4] fix --- .../Views/GenerateOutputView/GenerateOutputView+iOS.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift index 28d06a6..15aefcb 100644 --- a/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift +++ b/Csv2ImageApp/Csv2ImageApp/Views/GenerateOutputView/GenerateOutputView+iOS.swift @@ -114,6 +114,7 @@ struct GenerateOutputView_iOS: View { } } .background(Asset.lightAccentColor.swiftUIColor) + .frame(maxHeight: 200) GeometryReader { proxy in VStack(alignment: .center) {