Post

Replies

Boosts

Views

Activity

Unable to open a PDF document from with my app
My iOS app has a view where I list a number of links for further instructions. Linking to an external HTTP link is easy enough with a Link( destination:, label:). My challenge has been with local files which could be either an image or a PDF. I have this bit of code to deal with that: struct FileLinkView: View { let link: InstructionLink @State private var contentToShow: AnyView? var body: some View { VStack { Button(action: { if (contentToShow == nil) { openLocalFile(link.url) } else { contentToShow = nil } }) { Text(link.label) .foregroundColor(Color(.systemBlue)) } .contentShape(Rectangle()) // Make the entire button tappable .padding(5) contentToShow?.padding() } } func openLocalFile(_ path: String) { let cleanedPath: String if path.hasPrefix("file://") { cleanedPath = path.replacingOccurrences(of: "file://", with: "") } else if path.hasPrefix("file:") { cleanedPath = path.replacingOccurrences(of: "file:", with: "") } else { cleanedPath = path } let fileURL = URL(fileURLWithPath: cleanedPath) guard UIApplication.shared.canOpenURL(fileURL) else { return } if fileURL.pathExtension.lowercased() == "pdf" { print("FileLinkView trying to open PDF ", fileURL.absoluteString) DispatchQueue.main.async { UIApplication.shared.open(fileURL) } } else { print("FileLinkView trying to open image ", cleanedPath) if let uiImage = UIImage(contentsOfFile: cleanedPath) { contentToShow = AnyView(Image(uiImage: uiImage).resizable().scaledToFit()) } } } } The link url is in the form of "file://....." and points to files in subdirectories under the user's Documents directory. When opening the file with openLocalFile, I strip the url scheme off to use directly for images, and for a file URL for PDF. (Images work great. They show up inline as the 'contentToShow', which can be toggled. PDF files should be shown externally (like an http link) and so I may have to clean up the way 'contentToShow' is used. Originally, I planned to have the PDF inline like the image but thought it might be better to us an external viewer (is it Safari?) to do the job. I'm still pretty new to Swift.) Sadly, the PDF file does not show up even though the guard condition is passed and the resulting file URL printed in the console looks great. Here's a concrete example of that: FileLinkView trying to open PDF file:///Users/brian/Library/Developer/CoreSimulator/Devices/386A08A0-B581-4690-97E1-A5DCC532CDC3/data/Containers/Data/Application/B0491A25-69CE-47A1-B925-BB9E234383DF/Documents/checklog/blanks/My/Faves/instructions-brian/Express-Water-filter-manual.pdf I set the project's info.plist parameters "supports document browser" and "supports opening documents in place" to YES. The File app on my simulator can see and open the pdf in my app's directory structure so I believe that the file is accessible enough. And yet, when I click the button to show my PDF, nothing happens other than the printing in the console. Can someone please help me understand how to make this work? Thanks!
2
0
441
Feb ’24
Subclass not decoding through superclass
I'm having trouble decoding an array of subclassed super classes and getting the correct full subclass instance. When I run tests, the JSON is written correctly but I get a fatal error when trying to read and decode. It says "Thread 1: Fatal error: The data couldn’t be read because it is missing." I'm pretty sure that my nested decoding is incorrect. (At the advice of ChatGPT, I added the "type" attribute to help with know which subclass to actually init.). Currently I have only one subclass of CheckTask but there will be another once I know how to make this work. Here's what I'm attempting so far... import Foundation import Combine class CheckList: Identifiable, ObservableObject, Codable { let id: String var title: String @Published var tasks: [CheckTask] var domain: String .... enum CodingKeys: String, CodingKey { case id, title, tasks, domain, type } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) title = try container.decode(String.self, forKey: .title) domain = try container.decode(String.self, forKey: .domain) var tasksContainer = try container.nestedUnkeyedContainer(forKey: .tasks) var decodedTasks: [CheckTask] = [] while !tasksContainer.isAtEnd { let taskContainer = try tasksContainer.nestedContainer(keyedBy: CheckTask.CodingKeys.self) let type = try taskContainer.decode(String.self, forKey: .type) switch type { case "CheckItem": let item = try tasksContainer.decode(CheckItem.self) decodedTasks.append(item as CheckTask) // Add cases for other subclasses as needed default: // Handle unknown type or log a warning print("Unknown task type: \(type)") } } tasks = decodedTasks } .... } import Foundation class CheckTask: Identifiable, ObservableObject, Equatable, Codable { var id: UUID var locked: Bool .... required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) locked = try container.decode(Bool.self, forKey: .locked) } .... } import Foundation class CheckItem: CheckTask { var title: String @Published var checkmark: CheckMark var skippable: Bool = false var instructions: String var instructionLinks: [InstructionLink] .... // CodingKeys to specify custom mapping between Swift names and JSON keys enum CodingKeys: String, CodingKey { case type, title, checkmark, skippable, complete, instructions, instructionLinks } // Custom decoding method to decode the properties required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) checkmark = try container.decode(CheckMark.self, forKey: .checkmark) skippable = try container.decode(Bool.self, forKey: .skippable) instructions = try container.decode(String.self, forKey: .instructions) instructionLinks = try container.decode([InstructionLink].self, forKey: .instructionLinks) try super.init(from: decoder) } .... } I'm sure that the CheckTask and CheckItem decoding works but it's the CheckList that seems to be the issue. How can I do this correctly? Thanks!
1
0
338
Jan ’24
SwiftUi view responding to model changes
I developed a model and a view that responded to published changes in one of the model's data. As a button is clicked, the data changes and the button image also changed in response to that data change. This worked great in early testing since the model was wrapped by @ObservedObject but the ultimate goal was that the model object would be part of a containing object, and the view would be part of a view hierarchy so I changed @ObservedObject to @Binding and put the parent data object in a parent view using @State to make it the source of truth. That's when the button stopped changing its label in response to the data change, even though I knew the data was changing via debugging print statements. To investigate, I shorted up the hierarchy to just the initial content view and am using a single instance of my data and a simple button designed to highlight the (in)activity of the button label changes. Somehow, the model is no longer being observed correctly. Here's what I believe is the pertinent code... import Foundation enum CheckMark: Codable { case undone case unsure case done case skipped } // Created by Brian on 1/3/24. // import Foundation class CheckItem: Identifiable, ObservableObject, Codable { let id: UUID var title: String @Published var checkmark: CheckMark var instructions: String var instructionLinks: [String:String] var skippable: Bool = false // Initialize with values init(id: UUID, title: String, checkmark: CheckMark, instructions: String, instructionLinks: [String:String], skippable: Bool) { self.id = id self.title = title self.checkmark = checkmark self.instructions = instructions self.instructionLinks = instructionLinks self.skippable = skippable } // Initialize a fresh minimal item convenience init(id: UUID = UUID(), title: String) { self.init(id: id, title: title, checkmark: .undone, instructions: "", instructionLinks: [:], skippable: false) } // to help determine if completed for Completable protocol func isCompleted() -> Bool { return isDone() || isSkipped() } public func noInstructions() -> Bool { return instructions.isEmpty } public func isDone() -> Bool { return (checkmark == .done) } public func isUndone() -> Bool { return (checkmark == .undone) } public func isSkipped() -> Bool { return (checkmark == .skipped) } public func isUnsure() -> Bool { return (checkmark == .unsure) } public func isSkippable() -> Bool { return skippable } public func setDone() { checkmark = .done print("setting to ", checkmark) } public func setUndone() { checkmark = .undone print("setting to ", checkmark) } public func setSkipped() { checkmark = .skipped print("setting to ", checkmark) } public func setUnsure() { checkmark = .unsure print("setting to ", checkmark) } // CodingKeys enum for Codable enum CodingKeys: CodingKey { case id, title, checkmark, instructions, instructionLinks, skippable, locked } // Encode the CheckItem to JSON func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(title, forKey: .title) try container.encode(checkmark, forKey: .checkmark) try container.encode(instructions, forKey: .instructions) try container.encode(instructionLinks, forKey: .instructionLinks) try container.encode(skippable, forKey: .skippable) } // Initialize from JSON required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) title = try container.decode(String.self, forKey: .title) checkmark = try container.decode(CheckMark.self, forKey: .checkmark) instructions = try container.decode(String.self, forKey: .instructions) instructionLinks = try container.decode(Dictionary.self, forKey: .instructionLinks) skippable = try container.decode(Bool.self, forKey: .skippable) } } import SwiftUI struct ContentView: View { @State var checkItem = PreviewData.makeCheckItem(title: "Test Item") var body: some View { VStack { Button { switch checkItem.checkmark { case .undone: checkItem.setDone() print("undone toggled to ", checkItem.checkmark) case .done: checkItem.setUndone() print("done toggled to ", checkItem.checkmark) default: checkItem.setDone() print("unsure toggled to ", checkItem.checkmark) } } label: { switch checkItem.checkmark { case .undone: Image(systemName: "square") case .done: Image(systemName: "checkmark.square") case .unsure: Image(systemName: "square") .background(Color(UIColor.yellow)) .foregroundColor(Color(.red)) default: Image(systemName: "square").opacity(0.0) } } .onReceive(checkItem.$checkmark) { newCheckmark in print("Checkmark is \(newCheckmark)") } Text(checkItem.title) } .padding() } } The switch statement in the "label" is not executing, but the "onReceive" is working (for debugging) PreviewData.makeCheckItem looks like this... static func makeCheckItem(title: String, checkmark: CheckMark? = .undone, instructions: String? = "", instructionLinks: [String:String]? = [:], skippable: Bool? = false) -> CheckItem { let checkItem = CheckItem(title: title) checkItem.checkmark = checkmark ?? .undone checkItem.instructions = instructions ?? "" checkItem.instructionLinks = instructionLinks ?? [:] checkItem.skippable = skippable ?? false return checkItem } How can I make this simple button labeling respond to data changes as expected? (And is working when it is @ObservedObject, not @State?). If I understand this, I think I can expand upon the technique to get my full hierarchy working.
5
0
821
Jan ’24
Completion Handler Skipped
I see this happening in other threads but none of those solutions have worked for me. I'm new to Swift (my apologies) and attempting a simple test to get a string response from a service I have running on my machine. (This service show results from a browser and Postman so I'm sure it's working.) This is my code: let url = URL(string: "http://127.0.0.1:8080/stuff/ping")! var request = URLRequest(url: url) request.httpMethod = "GET" print("creating ping task...") let task = URLSession.shared.dataTask(with: request) { data, response, error in print("processing ping result...") if let data = data { let str = String(decoding: data, as: UTF8.self) print(str) } else if let error = error { print("HTTP Request Failed \(error)") } } print("resuming ping task...") task.resume() When I run that in a playground, it works! The console shows: creating ping task.... resuming ping task... processing ping result... One ping only, Vasily But from within a XCTest, it does not: Test Suite 'NetworkTests' started at 2023-06-02 08:24:46.007 Test Case '-[StuffTests.NetworkTests testPing]' started. creating ping task... resuming ping task... Test Case '-[StuffTests.NetworkTests testPing]' passed (0.002 seconds). Test Suite 'NetworkTests' passed at 2023-06-02 08:24:46.010. Executed 1 test, with 0 failures (0 unexpected) in 0.002 (0.002) seconds I've set the info of the project according to what I've seen in other posts so that it can access a local host (?): I appeal to you for help in understanding and fixing this. thank you!
3
0
1k
Jun ’23