import SwiftUI import SFSafeSymbols import UniformTypeIdentifiers struct ContentView: View { @Environment(\.colorScheme) var defaultColorScheme: ColorScheme @State var content: [CVInfo] let style: CVStyle init(content: [CVInfo], style: CVStyle) { self._content = .init(initialValue: content) self.style = style } @State var darkModeEnabled = true @State var didReadDarkMode = false @State var selectedLanguageIndex = 0 @State private var showExportFormatPicker = false var colorStyle: ColorScheme { darkModeEnabled ? .dark : .light } var info: CVInfo { content[selectedLanguageIndex] } var body: some View { VStack(alignment: .leading) { HStack { Picker("", selection: $selectedLanguageIndex) { ForEach(Array(content.enumerated()), id: \.element) { content in Text(content.element.language) .tag(content.offset) } }.frame(maxWidth: 100) Button(action: { showExportFormatPicker = true }) { Label("Export", systemSymbol: .squareAndArrowDown) .padding(3) .frame(maxHeight: 20) }.confirmationDialog( "Which format would you like?", isPresented: $showExportFormatPicker ) { Button("JSON data", action: exportJsonData) Button("PDF", action: exportPdf) Button("Image", action: exportImage) Button("Cancel", role: .cancel, action: {}) } message: { Text(" Choose the export format") } Button(action: importData) { Label("Import", systemSymbol: .docBadgePlus) .padding(8) .frame(maxHeight: 20) } Spacer() Toggle("Dark mode", isOn: $darkModeEnabled) .toggleStyle(SwitchToggleStyle()) } .padding([.top, .trailing]) .padding(.leading, 6) .padding(.bottom, 4) ScrollView(.vertical) { CV(info: info, style: style) }.frame(width: style.pageWidth) } .preferredColorScheme(colorStyle) .onAppear { guard !didReadDarkMode else { return } darkModeEnabled = defaultColorScheme == .dark didReadDarkMode = true } } private func importData() { guard let url = showOpenFilePanel() else { print("No url to import") return } let data: Data do { data = try Data(contentsOf: url) } catch { print("Failed to open file: \(error)") return } let newData: CVInfo do { newData = try JSONDecoder().decode(CVInfo.self, from: data) } catch { print("Failed to decode data: \(error)") return } guard let index = content.firstIndex(where: { $0.language == newData.language }) else { content.append(newData) selectedLanguageIndex = content.count - 1 return } content[index] = newData selectedLanguageIndex = index } private func exportPdf() { DispatchQueue.main.async { guard let url = showPdfSavePanel() else { print("No url to save PDF") return } savePDF(to: url) } } private func exportImage() { DispatchQueue.main.async { guard let url = showJpgSavePanel() else { print("No image url") return } self.saveImage(to: url) } } private func exportJsonData() { guard let url = showJsonSavePanel() else { print("No url to save data") return } saveJson(to: url) } private func showOpenFilePanel() -> URL? { let panel = NSOpenPanel() panel.allowedContentTypes = [.json] panel.canCreateDirectories = true panel.isExtensionHidden = false panel.allowsOtherFileTypes = false panel.title = "Load JSON" panel.message = "Choose a JSON file of resume data to import" panel.nameFieldLabel = "File name:" panel.nameFieldStringValue = "CV.json" let response = panel.runModal() guard response == .OK else { return nil } return panel.url } private func showJsonSavePanel() -> URL? { showSavePanel( type: .json, title: "Save JSON", fileName: "CV.json", message: "Choose a location to save a JSON file of the resume data") } private func showJpgSavePanel() -> URL? { showSavePanel( type: .jpeg, title: "Save image", fileName: "CV.jpg", message: "Choose a location to save an image of the resume") } private func showPdfSavePanel() -> URL? { showSavePanel( type: .pdf, title: "Save PDF", fileName: "CV.pdf", message: "Choose a location to save a PDF of the resume") } private func showSavePanel(type: UTType, title: String, fileName: String, message: String) -> URL? { let savePanel = NSSavePanel() savePanel.allowedContentTypes = [type] savePanel.canCreateDirectories = true savePanel.isExtensionHidden = false savePanel.allowsOtherFileTypes = false savePanel.title = title savePanel.message = message savePanel.nameFieldLabel = "File name:" savePanel.nameFieldStringValue = fileName let response = savePanel.runModal() guard response == .OK else { return nil } return savePanel.url } private var renderContent: some View { CV(info: info, style: style) .frame(width: style.pageWidth, height: style.pageHeight) .preferredColorScheme(colorStyle) } @MainActor private func saveImage(to url: URL) { let renderer = ImageRenderer(content: renderContent) renderer.scale = 3 guard let image = renderer.nsImage else { print("No image from renderer") return } let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil)! let bitmapRep = NSBitmapImageRep(cgImage: cgImage) let data = bitmapRep.representation(using: .jpeg, properties: [:])! do { try data.write(to: url) print("Data saved") } catch { print("Failed to save image: \(error)") } } @MainActor private func savePDF(to url: URL) { let renderer = ImageRenderer(content: renderContent) var didFinish = false renderer.render { size, context in var box = CGRect(x: 0, y: 0, width: size.width, height: size.height) guard let pdf = CGContext(url as CFURL, mediaBox: &box, nil) else { print("Failed to create CGContext") return } let options: [CFString: Any] = [ kCGPDFContextMediaBox: CGRect(origin: .zero, size: size) ] pdf.beginPDFPage(options as CFDictionary) context(pdf) pdf.endPDFPage() pdf.closePDF() didFinish = true } guard didFinish else { return } print("PDF created") } private func saveJson(to url: URL) { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data: Data do { data = try encoder.encode(info) } catch { print("Failed to encode data: \(error)") return } do { try data.write(to: url) } catch { print("Failed to write data: \(error)") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle) .frame(width: 600, height: 600 * sqrt(2)) } }