I am recently retired and learning Swift/SwiftUI to write a tracking program for my forty plus characters in the Star Trek Online game that I have played for years. I will have to enter the data manually as, to my knowledge STO has no API to retrieve a players accounts, character data.
I have many screens written and my SwiftData database set up. I am currently working on a screen to select the traits a character has available to them, (has available/unlocked). This data I am loading from a json file is the Master list and will not change for each trait type, (7 total). My screen has a VStack inside a ZStack which holds an HStack with a vstack and a list inside. the VStack to the left holds a button to select which Master trait list to display for selection. and boxes under it to hold the trait Images that have been selected for the characters build. (see included screen shot) The List on the right shows the selected trait type.
I am using a custom ListCell to display the info. I have a trait is active selection box that I wish to show that the trait is active and place that traits image into a Set that I can store in my data base and retrieve when I select the character it belongs to.
The problem I am trying to solve is how to send the selected trait id: String into the set and display it on the VStack side. I have tried several ways but not working. the last way I tried was to make a class that holds the sets I need and place it in the environment, but I must not fully understand how all that works properly, as it seems to make a set for each cell in my list when I only want to place the selected cell into the set. I have included sample data and the code I am working on. Note I am not trying to use the lists edit toggle feature to select my traits, but my own checkbox. Any help to point me in the right direction would be much appreciated. Thanks!
import SwiftUI
/// ST = Space Trait, GT = Ground Trait, SST = Starship Trait, SR = Space Reputation Trait, GR = Ground Reputation Trait, AGR = Acrive Ground Reputation Trait, ASR = Active Space ReputationTrait.
/// This enum also has an associated "title" String.
enum ActiveTraitType {
case GT // GT = Ground Trait
case ST // ST = Space Trait
case SST // SST = Starship Trait
case GR // GR = Ground Reputation Trait
case SR // SR = Space Reputation Trait
case AGR // AGR = Active Ground Reputation Trait
case ASR // ASR = Active Space Reputation Trait
}
// MARK: - Trait
struct Trait: Codable, Identifiable {
let id, traitType: String
let detailHeading, detail, detailedInfo, obtainedBy: String
let isActive: Bool
}
typealias Traits = [Trait]
@Observable
class traitSettings{
var selectedGroundTraits: Set<String> = []
//var selectedSpaceTraits: Set<String> = []
//var selectedStarshipTraits: Set<String> = []
//var selectedGroundReputationTraits: Set<String> = []
//var selectedSpaceReputationTraits: Set<String> = []
//var selectedActiveGroundReputationTraits: Set<String> = []
//var selectedActiveSpaceReputationTraits: Set<String> = []
}
struct TestTraitCellView: View {
@Environment(traitSettings.self) var gTTraits
/// GT = Ground Trait,
/// ST = Space Trait,
/// SST = Starship Trait,
/// SR = Space Reputation Trait,
/// GR = Ground Reputation Trait,
/// AGR = Active Ground Reputation Trait,
/// ASR = Active Space Reputation Trait.
var trait: Trait
/// Holds the ActiveTrait enum value. Should be initalized as GT to start.
var selectedTraitType: ActiveTraitType
/// Reflects the detailsShowing state. Are the Trait details being shown or not being shown.
@State private var detailsShowing = false
/// Reflects the traitIsSelected State. Is This Trait Selected or not?
@State private var traitIsSelected = false
/// Reflects the moreInfoShowing State. Has the Detail button been selected to show more details about this trait.
@State private var moreInfoShowing = false
/// This note gets placed into every Trait Cell.
var activeRepTraitNotes1 = "NOTE: All Space and Ground Trait values are based on the Traits ToolTip; Using a level 65 Alien Character, (No intrinsic traits), with No Ground or Space Skills selected, No Crew or Ship Mastery, No Ground or Space Equipment Slotted, with a 700 Day Veteran buff and no other Buffs applied. These numbers are taken while on Ground or in Space accordingly.nYour Characters values may differ based on Spiecies, Skills, Builds, and Buffs!"
var body: some View {
// Trait CellView.
HStack {
// Traits image to display.
traitImage
VStack(alignment: .leading, spacing: 2) {
traitCellHeader
detailsText
detailsButton
detailsStack
} // End of Vstack.
.padding(1) // padding is between image and text.
} // End of Hstack.
.onAppear {
traitIsSelected = trait.isActive
}
.border(Color.yellow,width: 2)
.background(Color.backBlue) // Add a Universal Color asset Hex #192D47 for this Color.
.listRowBackground(Color.black)
} // End of body.
}
struct ContentView: View {
var testTraits: [Trait] = Bundle.main.decode("testfileGT.json")
@State private var traitIsSelected = false
var body: some View {
List {
ForEach(testTraits) { trait in
TestTraitCellView(trait: trait, selectedTraitType: .GT)
}
}
.environment(traitSettings())
}
}
#Preview {
ContentView()
}
extension TestTraitCellView {
var traitImage: some View {
Image(systemName: trait.id)
.font(.largeTitle)
.foregroundStyle(Color.white)
.frame(width: 45, height: 60)
.padding(.leading, 8)
}
var traitCellHeader: some View {
HStack(alignment: .center) {
Text(trait.id).font(.headline).bold()
.frame(width: .infinity, alignment: .leading)
.padding([.top, .leading], 5)
.foregroundStyle(Color.yellow)
Spacer()
Image(systemName: trait.traitType == "Ground" ? "car.circle.fill" : "airplane.circle.fill")
.foregroundStyle(Color.white)
.frame(width: 25, height: 31)
.padding(.top, 2)
.padding(.trailing, 2)
.onTapGesture {
moreInfoShowing.toggle()
detailsShowing = true
}
}
.padding(1)
}
// MARK: - activeBox
var activeBox: some View {
HStack {
Image(systemName: traitIsSelected ? "checkmark.square.fill" : "square.fill")
.resizable()
.frame(width: 15, height: 15)
.foregroundColor(traitIsSelected ? Color.teal : Color.black)
.border(Color.blue)
Text(traitIsSelected ? "Trait is Active" : "Trait is Inactive")
.padding(.trailing, 5)
.font(.caption2)
.foregroundStyle(traitIsSelected ? Color.yellow : Color.white)
}
.onTapGesture {
traitIsSelected.toggle()
if traitIsSelected {
print("Placing (trait.id), trait into the characters, (selectedTraitType.title), trait Set.")
gTTraits.selectedGroundTraits.insert(trait.id)
print(gTTraits.selectedGroundTraits)
} else {
print("Removing (trait.id), trait from the characters, (selectedTraitType.title), trait Set.")
}
gTTraits.selectedGroundTraits.remove(trait.id)
print(gTTraits.selectedGroundTraits)
} // tap gesture.
}
// MARK: - details Button
var detailsButton: some View {
HStack {
Button(detailsShowing ? "▲ Less" : "▼ Details") {
detailsShowing.toggle()
if detailsShowing == false {
moreInfoShowing = false
}
} // button
.controlSize(.mini)
.buttonStyle(.borderedProminent)
Spacer()
activeBox
}
.padding(.bottom,10) // added padding to the bottom of hstack holding button.
}
// MARK: - details Stack.
var detailsStack: some View {
VStack {
if detailsShowing {
HStack {
Text(trait.detail)
.frame(width: .infinity)
Spacer()
}
.font(.caption2)
.foregroundStyle(Color.white)
HStack {
Text(trait.detailedInfo)
.frame(width: .infinity)
.multilineTextAlignment(.leading)
Spacer()
}
.font(.caption)
.foregroundStyle(Color.buttonBlue) // Add a Universal Color asset hex #6894B7 for this Color.
.padding(.bottom, 15)
.padding(.trailing, 6)
if moreInfoShowing {
VStack(alignment: .leading) {
HStack {
Text(trait.obtainedBy)
.font(.caption2)
.frame(width: .infinity)
.multilineTextAlignment(.leading)
.foregroundStyle(Color.white)
.padding(.bottom, 15)
.padding(.trailing, 6)
Spacer()
}
Text("(activeRepTraitNotes1)")
.font(.caption2)
.frame(width: .infinity)
.multilineTextAlignment(.leading)
.foregroundStyle(Color.white).bold()
.padding(.bottom, 15)
.padding(.trailing, 6)
}
} // moreinfoshowing if.
} // End of if.
} // End of vstack.
}
var detailsText: some View {
HStack(alignment: .top) {
Text(trait.detailHeading)
.font(.caption2)
.multilineTextAlignment(.leading)
.foregroundStyle(Color.white)
.padding(.vertical, 3)
.padding(.bottom, 2)
}
}
}
// Learned this from Paul Husdon, Hacking with Swift.
extension Bundle {
func decode<T: Codable>(_ file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Filed to locate (file) in the bundle!")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Filed to load (file) from bundle!")
}
let decoder = JSONDecoder()
do {
return try decoder.decode(T.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
fatalError("Failed to decode (file) from bundle due to missing key ' (key.stringValue)' - (context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
fatalError("Failed to decode (file) from bundle due to type mismatch - (context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
fatalError("Failed to decode (file) from bundle due to missing (type) value - (context.debugDescription)")
} catch DecodingError.dataCorrupted(_) {
fatalError("Failed to decode (file) from bundle because it appears to be invalid JSON.")
} catch {
fatalError("Failed to decode (file) from bundle: (error.localizedDescription)")
}
// if we are here then use our data!
// send back our dictionary of traits
// return loaded
/* This is the test data for the JSON file. testfileGT.json
[
{
"id": "Master-Dice List.GT",
"traitType": "Space",
"detailHeading": "This cell here for testing only! All Trait info has been entered! All Trait info has been entered! All Trait info has been entered!",
"detail": "Any detail you want!",
"detailedInfo": "More detail info or leave blank.",
"obtainedBy": "How the trait is obtained goes here. Place holder.",
"isActive": false
},
// Remove this note before placing in your json file. This is the actual data for the Enlightened Space Trait.
{
"id": "die.face.1",
"traitType": "Space",
"detailHeading": "+Exotic Damg. +Hull Regen",
"detail": "Space Trait: Increases Exotic Damage and Hull Regeneration.",
"detailedInfo": "+15% Exotic Damage n+15% Hull Regeneration",
"obtainedBy": "Obtained from:n* [Genetic Resequencer - Space Trait: Enlightened] pack, a random reward from [Deep Space Nine Lock Box]n* [Infinity Prize Pack: Personal Trait (Space)]n* Exchange in the Reward Packs tab by searching for Enlightened",
"isActive": false
},
{
"id": "die.face.2",
"traitType": "Ground",
"detailHeading": "This cell here for testing only! All Trait info has been entered! All Trait info has been entered! All Trait info has been entered!",
"detail": "Any detail you want!",
"detailedInfo": "More detail info or leave blank.",
"obtainedBy": "How the trait is obtained goes here. Place holder.",
"isActive": false
},
{
"id": "die.face.3",
"traitType": "Ground",
"detailHeading": "This cell here for testing only! All Trait info has been entered! All Trait info has been entered! All Trait info has been entered!",
"detail": "Any detail you want!",
"detailedInfo": "More detail info or leave blank.",
"obtainedBy": "How the trait is obtained goes here. Place holder.",
"isActive": false
},
{
"id": "die.face.4",
"traitType": "Space",
"detailHeading": "This cell here for testing only! All Trait info has been entered! All Trait info has been entered! All Trait info has been entered!",
"detail": "Any detail you want!",
"detailedInfo": "More detail info or leave blank.",
"obtainedBy": "How the trait is obtained goes here. Place holder.",
"isActive": false
},
{
"id": "die.face.5",
"traitType": "Space",
"detailHeading": "This cell here for testing only! All Trait info has been entered! All Trait info has been entered! All Trait info has been entered!",
"detail": "Any detail you want!",
"detailedInfo": "More detail info or leave blank.",
"obtainedBy": "How the trait is obtained goes here. Place holder.",
"isActive": false
},
{
"id": "die.face.6",
"traitType": "Space",
"detailHeading": "This cell here for testing only! All Trait info has been entered! All Trait info has been entered! All Trait info has been entered!",
"detail": "Any detail you want!",
"detailedInfo": "More detail info or leave blank.",
"obtainedBy": "How the trait is obtained goes here. Place holder.",
"isActive": false
},
]
*/
}
}
extension ActiveTraitType {
var title: String {
switch self {
case .GT:
return "PersonalGroundTraits"
case .ST:
return "PersonalSpaceTraits"
case .SST:
return "Starship Traits"
case .GR:
return "GroundReputationTraits"
case .SR:
return "SpaceReputationTraits"
case .AGR:
return "ActiveGroundReputationTraits"
case .ASR:
return "ActiveSpaceReputationTraits"
}
}
}