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!
Post
Replies
Boosts
Views
Activity
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!
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.
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!