I sent a 1.0 version of my app through review but decided it wasn't ready so I rejected it. Now I've upload 1.1 which shows in TestFlight and distributes to testers okay but I don't see it in my apps listing. I wonder why not. And will it inherit all the stuff I put into the 1.0, inclusing screenshots, or do I need to start that over again. I thought 1.1 would just continue from 1.0. How is this supposed to work (this is my first app)?
Post
Replies
Boosts
Views
Activity
Hm, I used to get more help from this forum. Oh well, I managed to find a solution using a sheet and WKWebView. It's probably not what an expert would do but it should suffice.
As an update, I tried to use the Link like a http url and am reserving my FileLinkView for images only (for now, as a test), hoping the Link view was clever enough to deal with both http and file url schemes. Here's that code...
struct InstructionLinksView: View {
let links: [InstructionLink]
var body: some View {
ForEach(links, id: \.url) { link in
if link.url.hasPrefix("http") {
Link(destination: URL(string: link.url)!, label: {
Text(link.label)
.foregroundColor(Color(.systemBlue))
})
} else if link.url.hasPrefix("file") && link.url.hasSuffix("pdf") {
Link(destination: URL(string: link.url)!, label: {
Text(link.label)
.foregroundColor(Color(.systemBlue))
})
} else if link.url.hasPrefix("file") {
FileLinkView(link: link)
}
}
}
}
No joy. I still get no PDF document. I'm missing something. Thanks for your help!
I believe I found a solution to this. The error I was getting was caused by getting the container for the JSON data too many times, using up the data out of step of my intent. I guess it has something to to with how data is streamed from the file into the decoder.
The way out is ...
To break encapsulation a bit by making the CheckTask super class aware of all keys of of any of it's subclasses so that when the decoding process gets a nestedContainer, it's ready to go with all key knowledge whether it uses them all or not. that means the same container can be used for the decoding of any subclass.
and
Each subclass implements an extra init function that takes that container as an argument, then specifically decodes from there.
Here are some pertinent code snippets...
class CheckList: Identifiable, ObservableObject, Codable {
let id: String
var title: String
@Published var tasks: [CheckTask]
var domain: String
....
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)
// get the CheckTasks even though we don't know what each one is yet
var tasksContainer = try container.nestedUnkeyedContainer(forKey: .tasks)
tasks = []
// Iterate through the array of objects, determine the type, and decode accordingly
while !tasksContainer.isAtEnd {
// Because we don't know which subclass yet, we toss all possible keys at the taskContainer.
// We only get one bite at the apple and it has to be prepared to decode anything.
// See CheckTask.CodingKeys for more info.
let taskContainer = try tasksContainer.nestedContainer(keyedBy: CheckTask.CodingKeys.self)
let type = try taskContainer.decode(CheckType.self, forKey: .type)
// Now that we know the type, we choose how to init the correct class,
// using a convenience method that takes a container,
// then append to the array of tasks.
switch type {
case .CheckItem:
let item = try CheckItem(container: taskContainer)
tasks.append(item)
default:
// Handle unknown type or log a warning
print("Unknown task type:.")
}
}
}
}
class CheckTask: Identifiable, ObservableObject, Equatable, Codable {
var id: UUID
var locked: Bool
var type: CheckType
....
// CodingKeys to specify custom mapping between Swift names and JSON keys
// I hate to break encapsulation like this but, hey, that's decoding for ya.
enum CodingKeys: String, CodingKey {
// common keys
case id, locked, type
// CheckItem keys
case title, checkmark, skippable, complete, instructions, instructionLinks
// CheckGroup keys
case items
}
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)
type = try container.decode(CheckType.self, forKey: .type)
}
}
enum CheckType: Codable {
case CheckItem
case CheckGroup
case Unknown
}
class CheckItem: CheckTask {
var title: String
@Published var checkmark: CheckMark
var skippable: Bool = false
var instructions: String
var instructionLinks: [InstructionLink]
....
// Initialize with values
init(id: UUID, title: String, checkmark: CheckMark, skippable: Bool, locked: Bool, instructions: String?, instructionLinks: [InstructionLink]?) {
self.title = title
self.checkmark = checkmark
self.skippable = skippable
self.instructions = instructions ?? ""
self.instructionLinks = instructionLinks ?? []
super.init(id: id, locked: locked, type: CheckType.CheckItem)
}
/// Custom decoding method to decode the properties from a JSON container. This is used by the CheckList decode
/// so that it can handle instantiating subclasses of CheckTask blindly since it doesn't know there are subclasses
/// and can't direct specify a type.
/// - Parameter container: <#container description#>
convenience init(container: KeyedDecodingContainer<CodingKeys>) throws {
self.init(id: try container.decode(UUID.self, forKey: .id),
title: try container.decode(String.self, forKey: .title),
checkmark: try container.decode(CheckMark.self, forKey: .checkmark),
skippable: try container.decode(Bool.self, forKey: .skippable),
locked: try container.decode(Bool.self, forKey: .locked),
instructions: try container.decode(String.self, forKey: .instructions),
instructionLinks: try container.decode([InstructionLink].self, forKey: .instructionLinks)
)
}
}
So now I'll implement another subclass of CheckTask called CheckGroup and see how I can break stuff then.
Hey, I found that if I change "List" in the CheckListView to "VStack" it all behaves as expected. Not sure why, something to research, but at least I can ask a more intelligent question about this now.
This has uncovered another issue that I think is related somehow. The CheckItemCardView works fine on it's own but when it is in a list of card views, pressing one of the card buttons acts like all of the card's buttons are activated, making for strange behavior. below are the two views I'm talking about.
import SwiftUI
struct CheckListView: View {
@ObservedObject var checkList: CheckList
var body: some View {
List {
ForEach(checkList.items.indices, id: \.self) { index in
CheckItemCardView(checkItem: checkList.items[index], index: index)
}
}
}
}
struct CheckListView_Previews: PreviewProvider {
static var checkList = PreviewData.makeCheckList()
static var previews: some View {
CheckListView(checkList: checkList )
}
}
import SwiftUI
struct CheckItemCardView: View {
@ObservedObject var checkItem: CheckItem
let index: Int
var completed: Bool = false
@State var isPresentingInstructionView = false
var body: some View {
HStack {
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 \(checkItem.checkmark) and new one \(newCheckmark) in card \(index)")
}
Text(checkItem.title)
.strikethrough(checkItem.isSkipped())
Spacer()
HStack (alignment: .bottom) {
Button {
isPresentingInstructionView = true
print("isPresentingInstructionView set to true")
} label: {
Label("how-to", systemImage: "questionmark.bubble")
}
.labelStyle(VerticalLabelStyle())
.hidden(checkItem.noInstructions())
Button {
if (!checkItem.isSkipped()) {
checkItem.setSkipped()
print("skipping, checkmark set to ", checkItem.checkmark)
} else if (checkItem.isSkipped()) {
checkItem.setUndone()
print("unskipping, checkmark set to ", checkItem.checkmark)
}
} label: {
if (!checkItem.isSkipped()) {
Label("skip", systemImage: "arrowshape.bounce.right")
} else if (checkItem.isSkipped()) {
Label("unskip", systemImage: "arrowtriangle.backward")
}
}
.labelStyle(VerticalLabelStyle())
.hidden(!checkItem.skippable)
}
.foregroundColor(Color(.systemBlue))
}
.font(.title)
.sheet(isPresented: $isPresentingInstructionView) {
VStack {
ScrollView {
Text(checkItem.instructions)
.padding()
Section(header: Text("External Links")
.font(.title3) ) {
ForEach(checkItem.instructionLinks.sorted(by: >), id: \.key) { key, value in
Link(destination: URL(string: key)!, label: {
Text(value)
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button("Done", systemImage: "circle.inset.filled") {
isPresentingInstructionView = false
print("I read your dumb instructions")
}
.font(.title2)
}
}
}
}
// https://medium.com/macoclock/make-more-with-swiftuis-label-94ef56924a9d
struct VerticalLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .center, spacing: 2) {
configuration.icon
configuration.title.font(.callout)
}
}
}
extension View {
func hidden(_ shouldHide: Bool) -> some View {
opacity(shouldHide ? 0 : 1)
}
}
struct CheckItemCardView_Previews: PreviewProvider {
static var checkItem = PreviewData.makeLoneItem()
static var previews: some View {
CheckItemCardView(checkItem: checkItem, index: 1 )
}
}
so when I click on the first button, it also opens up the sheet and sets the checkmark value to "skipped", not at all what I want. And, again, this doesn't happen if I run just one CheckItemCardView all by itself. Can you explain why the list would make one button press activate all buttons in the card? Thanks in advance.
That helped quite a lot, thank you! Do I still need to use the $ when referring to CheckItem? Not sure if that was purely a struct thing or if it applies to classes as well.
Quinn! Thank you!
I wasn't aware that XCTest had a different lifecycle so thank you for opening my eyes. As a follow up question, if expectations or concurrency are not needed in the 'real' app, are they still considered best practice?
Thank you for getting me out of this jam.