SwiftUI: Match answer index with option index in Quiz game - swiftui

I'm building a quiz game and am fairly new to SwiftUI. When the quiz is over, I'd like the user to be able to see all the correct answers. Basically, I want to list all questions with the correct answer, but I'm not sure how to match the answer index (Int) with the correct option (String).
Any help is appreciated!
Here's my JSON Structure:
[
{
"id": "1",
"question": "Heres a question title",
"category": "sports",
"answer": 0,
"options": [
"A",
"B",
"C",
"D"
]
}
...
]
Here's my Model:
struct Question: Identifiable, Codable, Hashable {
var id: String
var question: String
var category: String
var answer: Int
var options: [String]
enum CodingKeys: String, CodingKey {
case id, question, category, answer, options
}
static var allQuestions: [Question] = Bundle.main.decode("quizquestions2022.json")
}
And here's the sheet view where I want to list all answers:
struct AnswerSheetView: View {
#StateObject var gameManager = GameManagerVM()
#Environment(\.presentationMode) var presentationMode
var body: some View {
ScrollView(showsIndicators: false) {
ForEach(gameManager.allQuestions, id: \.self) { question in
VStack(spacing: 16) {
CategoryTagView(name: question.category, size: 12)
Text(question.question)
Text("\(question.answer)")
.foregroundColor(Color(UIColor.systemGreen))
}
.font(.system(size: 20, weight: .bold, design: .rounded))
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding()
.background(Color(UIColor.systemGray6))
.cornerRadius(24)
}
}
.padding()
.background(Color(UIColor.black).ignoresSafeArea())
.navigationTitle("Facit")
.navigationBarTitleDisplayMode(.inline)
}
}

you could try something like this approach, to match the answer index (Int) with the correct option (String):
struct Question: Identifiable, Codable, Hashable {
var id: String
var question: String
var category: String
var answer: Int
var options: [String]
enum CodingKeys: String, CodingKey {
case id, question, category, answer, options
}
// this should be in your GameManagerVM, not here
static var allQuestions: [Question] = Bundle.main.decode("quizquestions2022.json")
func theAnswer() -> String { // <--- here
return (answer >= 0 && answer < options.count) ? options[answer] : ""
}
}
struct AnswerSheetView: View {
#StateObject var gameManager = GameManagerVM()
#Environment(\.presentationMode) var presentationMode
var body: some View {
ScrollView(showsIndicators: false) {
ForEach(gameManager.allQuestions, id: \.self) { question in
VStack(spacing: 16) {
CategoryTagView(name: question.category, size: 12)
Text(question.question)
// --- here
Text(question.theAnswer()).foregroundColor(Color(UIColor.systemGreen))
}
.font(.system(size: 20, weight: .bold, design: .rounded))
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding()
.background(Color(UIColor.systemGray6))
.cornerRadius(24)
}
}
.padding()
.background(Color(UIColor.black).ignoresSafeArea())
.navigationTitle("Facit")
.navigationBarTitleDisplayMode(.inline)
}
}

Related

Touch events seemingly not registering at top of screen

I'm seeing very strange behavior within a view. Here's my layout:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
#available(iOS 15.0, *)
struct EventDetailView: View {
#Binding var event: EventRecord
#Binding var editing: Bool
#FocusState var textIsFocused: Bool
var body: some View {
VStack {
TextField(
"Event text",
text: $event.text
)
.focused($textIsFocused)
.disabled(!editing)
.padding()
DatePicker("Event Date:", selection: $event.date)
.disabled(!editing)
.padding()
Toggle("Goal is Reached?", isOn: $event.achievesKR)
.disabled(!editing)
.padding()
HStack {
Text("Notes:")
Spacer()
}
.padding()
TextEditor(text: $event.notes)
.disabled(!editing)
.padding()
Spacer()
}
}
}
struct EventRecord: Identifiable, Equatable {
typealias ID = Identifier
struct Identifier: Identifiable, Equatable, Hashable {
typealias ID = UUID
let id: UUID = UUID()
}
let id: ID
var keyResults: [KeyResult.ID]
var date: Date
var text: String
var notes: String
var achievesKR: Bool
init(
id: ID = ID(),
keyResults: [KeyResult.ID],
date: Date = Date(),
text: String,
notes: String = "",
achievesKR: Bool
) {
self.id = id
self.keyResults = keyResults
self.date = date
self.text = text
self.notes = notes
self.achievesKR = achievesKR
}
}
So this works perfectly when I run it as an iPad app, but when I run it on the simulator, the the top toggle doesn't respond to text input.
The strange thing is, when I simply duplicate the toggle, the top one doesn't work and the bottom one works perfectly:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
It seems like this should be totally unrelated to the touch behavior of the other views.
Btw this is being displayed in the context of a navigation view.
Is there anything that can explain this? And how can I get it working without adding this extra view on top?
edit: Here's a gif of this behavior being demonstrated. The two controls are exactly the same, but the lower one responds to touch and the upper one does not.

Read Json and Add to picker swiftUI

struct Test: View {
#ObservedObject var datas2 = ReadData2()
#State var selectedIndex2 = 0
var body: some View {
VStack{
List{
HStack{
Text("From")
.foregroundColor(Color("Color"))
.fontWeight(.bold)
.font(.system(size: 20))
.padding(.leading, 10.0)
.frame(width: nil, height: nil, alignment: .leading)
Picker(selection: $selectedIndex2, label: Text("")) {
ForEach(0 ..< datas2.cities.count, id: \.self) {
Text(datas2.cities[$0].name)
}
}.foregroundColor(Color("Color"))
.padding(.trailing)
}
}
}
}
struct City: Codable, Identifiable {
enum CodingKeys: CodingKey {
case name
}
var id = UUID()
var name: String
}
class ReadData2: ObservableObject {
#Published var cities = [City]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "cities", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let cities = try? JSONDecoder().decode([City].self, from: data!)
self.cities = cities!
}
}
and I have a json file called cities that is like this :
{
"country": "AD",
"name": "Sant Julià de Lòria",
"lat": "42.46372",
"lng": "1.49129"
},
{
"country": "AD",
"name": "Pas de la Casa",
"lat": "42.54277",
"lng": "1.73361"
},
{
"country": "AD",
"name": "Ordino",
"lat": "42.55623",
"lng": "1.53319"
},
{
"country": "AD",
"name": "les Escaldes",
"lat": "42.50729",
"lng": "1.53414"
},
{
"country": "AD",
"name": "la Massana",
"lat": "42.54499",
"lng": "1.51483"
},
My problem is that cites are too much data, and once I press the picker it takes a long time to read data and post data inside the picker, is there a better way for making it faster ?
and I am trying to make it searchable throught a search text taking the first letter of the city and it is not working !
I hope you can help me !
With very large number of items, I would suggest using a List or a LazyVStack that are optimised for that, instead of a Picker.
Something like the following example code.
Note, you will have to adjust the UI for your purpose.
struct ContentView: View {
var body: some View {
Test()
}
}
struct Test: View {
#StateObject var datas2 = ReadData2()
#State var selectedCity: City?
#State private var searchQuery: String = ""
var body: some View {
VStack (spacing: 20) {
TextField("city search", text: $searchQuery).padding(5)
.overlay(RoundedRectangle(cornerRadius: 15).stroke(Color.blue, lineWidth: 1))
.foregroundColor(.blue)
.frame(width: 160)
.padding(.top, 20.0)
HStack {
Text("From")
.fontWeight(.bold)
.font(.system(size: 20))
.padding(.leading, 10.0)
Spacer()
ScrollView {
LazyVStack {
ForEach(datas2.cities.filter{searchFor($0.name)}.sorted(by: { $0.name < $1.name })) { city in
Text(city.name).foregroundColor(selectedCity == city ? .red : .blue)
.onTapGesture {
selectedCity = city
}
}
}
}.frame(width: 222, height: 111)
}
Spacer()
}
}
private func searchFor(_ txt: String) -> Bool {
return (txt.lowercased(with: .current).hasPrefix(searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(with: .current)) || searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
struct City: Codable, Identifiable, Hashable {
enum CodingKeys: CodingKey {
case name
}
var id = UUID()
var name: String
}
class ReadData2: ObservableObject {
#Published var cities = [City]()
init() {
loadData()
}
func loadData() {
if let url = Bundle.main.url(forResource: "cities", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
cities = try JSONDecoder().decode([City].self, from: data)
} catch {
print(" error:\(error)")
}
}
}
}

TabView with custom views using ForEach in SwiftUI

I previously asked a question about how to link an array of TabButtons to .tag() here: .tag() in TabView in SwiftUI challenge, but now I want to customize each TabView to have different information and not just the one line of text that reads from the enum cases that was created. In the below code I added additional views that I would like to populate with each TabView. I want to be able to insert additional tabs/views in the future. I've created the custom views at the bottom, but not sure how to link with the enums and then use a ForEach in TabView.
import SwiftUI
struct ContentView: View {
var body: some View {
CustomTabView()
}
}
struct CustomTabView: View {
#State var selectedTab = ViewSelect.HomeView // This is an enum defined below.
#State var viewData : AllViewData!
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
ForEach(ViewSelect.allCases, id: \.self) { view in // This iterates through all of the enum cases.
ViewsCardView(viewData: view.allViewData)
.tag(view.rawValue) // by having the tag be the enum's raw value,
// you can always compare enum to enum.
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.ignoresSafeArea(.all, edges: .bottom)
ScrollView(.horizontal, showsIndicators: false, content: {
HStack {
ForEach(ViewSelect.allCases ,id: \.self){viewSelect in
TabButton(viewSelect: viewSelect, selectedTab: $selectedTab)
}
}
})
.padding(.horizontal, 25)
.padding(.vertical, 5)
.background(Color.white)
.clipShape(Capsule())
.shadow(color: Color.black.opacity(0.15), radius: 5, x: 5, y: 5)
.shadow(color: Color.black.opacity(0.15), radius: 5, x: -5, y: -5)
.padding(.horizontal)
}
.background(Color.black.opacity(0.05).ignoresSafeArea(.all, edges: .all))
}
}
struct TabButton: View {
let viewSelect: ViewSelect
#Binding var selectedTab: ViewSelect
var body: some View {
Button(action: {selectedTab = viewSelect}) {
Image(systemName: viewSelect.tabIcon)
.renderingMode(.template)
// this compares the selection to the button's associated enum.
// The enum provides the image name to the button,
// but we are always dealing with a case of the enum.
.opacity(selectedTab == viewSelect ? (1) : (0.5))
.padding()
}
}
}
struct ViewsCardView: View {
#State var viewData: AllViewData
var body: some View {
VStack {
Text(viewData.name)
}
}
}
struct AllViewData : Identifiable, Hashable {
var id = UUID().uuidString
var name : String
var text : Int
}
public enum ViewSelect: String, CaseIterable {
case HomeView, EnvelopeView, FolderView, SettingsView
var tabIcon: String {
switch self {
case .HomeView:
return "house"
case .EnvelopeView:
return "envelope"
case .FolderView:
return "folder"
case .SettingsView:
return "gear"
}
}
var allViewData: AllViewData {
return AllViewData(name: self.rawValue, text: self.hashValue)
}
}
struct DataForViews : Identifiable {
var id = UUID().uuidString
var someText : SomeText
var someImage : SomeImage
var someData : String
var moreText : String
enum SomeText : String, CaseIterable {
case friends
case enemies
case neutral
case both
}
enum SomeImage : String, CaseIterable {
case friends = "person.3"
case enemies = "person.fill.xmark"
case neutral = "person"
case both = "person.circle"
}
}
struct HomeView: View {
#State var viewData = DataForViews(someText: .friends, someImage: .friends, someData: "Some Data", moreText: "More Text")
var body: some View {
VStack {
Text(viewData.someText.rawValue)
Image(systemName: viewData.someImage.rawValue)
.resizable()
.frame(width: 40, height: 40)
Text(viewData.someData)
Text(viewData.moreText)
}
}
}
struct EnvelopeView: View {
#State var viewData = DataForViews(someText: .enemies, someImage: .enemies, someData: "Some Data", moreText: "More Text")
var body: some View {
VStack {
Text(viewData.someText.rawValue)
Image(systemName: viewData.someImage.rawValue)
.resizable()
.frame(width: 40, height: 40)
Text(viewData.moreText)
}
}
}
struct FolderView: View {
#State var viewData = DataForViews(someText: .neutral, someImage: .neutral, someData: "Some Data", moreText: "More Text")
var body: some View {
VStack {
Text(viewData.someText.rawValue)
Image(systemName: viewData.someImage.rawValue)
.resizable()
.frame(width: 40, height: 40)
Text(viewData.someData)
Text(viewData.moreText)
}
}
}
struct SettingsView: View {
#State var viewData = DataForViews(someText: .both, someImage: .both, someData: "Some Data", moreText: "More Text")
var body: some View {
VStack {
Text(viewData.someText.rawValue)
Image(systemName: viewData.someImage.rawValue)
.resizable()
.frame(width: 40, height: 40)
Text(viewData.someData)
}
}
}
I think I figured this out. It took me a lot of tinkering and I came here to stack overflow to try and get further clarity. But the below solution should work. I created a variable in the HomeView() that linked the ViewSelect as an enum, then I initialized the different options with an array in the DataForViews struct and linked the selection to the ViewSelect enum which is tied to the tabItems.
Then in the ContentView, I followed the same pattern except used the ForEach loop on the HomeView() and for the .tag() it is tied to the selection within the DataForViews.
struct CustomTabView: View {
#State var selectedTab = ViewSelect.HomeView // This is an enum defined below.
#State var viewData : AllViewData!
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
ForEach(viewDataStuff) { view in
HomeView(viewData: view)
.tag(view.selection)
}
struct DataForViews : Identifiable, Hashable {
var id = UUID().uuidString
var someText : SomeText
var someImage : SomeImage
var someData : String
var moreText : String
var selection : ViewSelect
enum SomeText : String, CaseIterable {
case friends
case enemies
case neutral
case both
}
enum SomeImage : String, CaseIterable {
case friends = "person.3"
case enemies = "person.fill.xmark"
case neutral = "person"
case both = "person.circle"
}
}
var viewDataStuff = [
DataForViews(someText: .friends, someImage: .friends, someData: "Text", moreText: "Text", selection: .HomeView),
DataForViews(someText: .enemies, someImage: .enemies, someData: "Text", moreText: "Text", selection: .EnvelopeView),
DataForViews(someText: .neutral, someImage: .neutral, someData: "Text", moreText: "Text", selection: .FolderView),
DataForViews(someText: .both, someImage: .both, someData: "Text", moreText: "Text", selection: .SettingsView),
]
struct HomeView: View {
#State var viewData : DataForViews
var body: some View {
VStack {
Text(viewData.someText.rawValue)
Image(systemName: viewData.someImage.rawValue)
.resizable()
.frame(width: 40, height: 40)
Text(viewData.someData)
Text(viewData.moreText)
}
}
}
'''

Binding two ForEach loop to update each item cell

This is my second post and I need your help as much as possible. I am creating a favorite button on my parent view and detail view. I need both buttons to work correspondent to each other. When I marked favorite on the ForEach loop of my parent view, I want to show the item is favorited in my detail view. Also, I can unfavorite or favorite from my detail view vice vasa. It is really hard for me to figure out how to bind those two ForEach loops. Below I provide an example of my codes. If you want to test with my full code, you can access it here: Making favorite button from several layers and binding two list using EnvironmentObject
struct Data: Identifiable {
let id = UUID()
let number: Int
var name1: String
let name2: String
}
public struct DataList {
static var dot = [
Data(number: 1,
name1: "Pasian Phatna",
name2: "Praise God, from whom All Blessings Flow"),
Data(number: 2,
name1: "Itna Kumpi, Ka Tuu-Cing Pa",
name2: "The King of Love My Shephaerd Is (Dominus Regit Me)"),
Data(number: 3,
name1: "Kumpipa Bia Un",
name2: "O Worship the King"),
Data(number: 4,
name1: "Pa Tung Min Than'na Om Hen",
name2: "Gloria Patri (1st Tune)"),
Data(number: 5,
name1: "Pa Tung Min Than'na Om Hen",
name2: "Gloria Patri (2nd Tune)")
]
}
struct ParentView: View {
#State var datas: [Data] = DataList.dot
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(spacing: 5) {
ForEach (datas, id: \.id) { data in
MainData(data: data)
Divider()
.padding(.all)
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct MainData: View {
#State var data: Data
#State var selectedFavoriteSong: Bool = false
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.number)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name1)
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
Text(data.name2)
.font(.title3)
.fontWeight(.medium)
.foregroundColor(.secondary)
.italic()
}
.padding(.horizontal)
.multilineTextAlignment(.center)
}
}
Please consider, the Search() below will pop up when I tapped the search icon (which is not presented here). My point is the Search() is not directly connect to the ParentView() but the DetailView() is embedded in the Search().
struct Search: View {
#State var datas: [Data] = DataList.dot
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach (datas, id: \.id) { data in
NavigationLink(
destination: DetailView(data: data),
label: {
Text("Search")
})
}
}.padding(.horizontal)
}
}
}
}
struct DetailView: View {
#State var data: Data
#State var selectedFavoriteSong: Bool = false
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.name1)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name2)
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
}
.padding(.horizontal)
.multilineTextAlignment(.center)
Spacer()
}
}
So, I want to connect the parent view and the detail view with some kind of binding property. But there is impossible to connect these two. I can store
#State var selectedFavoriteSong: Bool = false
inside the EnvironmentObject. But when I click favorite, all the items inside the ForEach loop are selected. Please help me on this issue. If you need a full code, the above link will direct to my first post. Thank you.
I'd suggest storing all of your data in an ObservableObject that is owned by the parent view and then can get passed into subviews (either explicitly or via an EnvironmentObject):
class DataSource : ObservableObject {
#Published var data : [Data] = DataList.dot
#Published var favoritedItems: Set<UUID> = []
func favoriteBinding(forID id: UUID) -> Binding<Bool> {
.init {
self.favoritedItems.contains(id)
} set: { newValue in
if newValue {
self.favoritedItems.insert(id)
} else {
self.favoritedItems.remove(id)
}
}
}
}
For example:
struct ParentView : View {
#StateObject var dataSource = DataSource()
var body: some View {
VStack {
Search(dataSource: dataSource)
}
}
}
Note that the data source stores a list of IDs that have been favorited. It uses a custom binding that can pass the boolean value down to a detail view:
struct Search: View {
#ObservedObject var dataSource : DataSource
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach (dataSource.data, id: \.id) { data in
NavigationLink(
destination: DetailView(data: data,
selectedFavoriteSong: dataSource.favoriteBinding(forID: data.id)),
label: {
Text(data.name1)
})
}
}.padding(.horizontal)
}
}
}
}
struct DetailView: View {
var data : Data
#Binding var selectedFavoriteSong : Bool
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if self.selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.name1)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name2 ?? "")
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
}
.padding(.horizontal)
.multilineTextAlignment(.center)
Spacer()
}
}

SwiftUI Picker Item multilines

How do I do text wrapping?
Here is my code:
struct ContentView: View {
private let items: [String] = [
"OneLineLongggggggggggggggggggggggggggggggggggggggggg",
"TwoLinesLonggggggggggggg\nLongggggggggggggggg",
"ThreeLinesLonggggggggggggg\nLongggggggggggggggg\nLongggggggggggggggg"
]
#State private var text: String = ""
var body: some View {
VStack {
Picker("Select Text", selection: self.$text) {
ForEach(self.items, id: \.self) {
Text($0)
.tag($0)
}
}
Text("select: \(self.text)")
}
}
}
Here is the result:
If I add fixSize then the elements run over each other:
struct ContentView: View {
private let items: [String] = [
"OneLineLongggggggggggggggggggggggggggggggggggggggggg",
"TwoLinesLonggggggggggggg\nLongggggggggggggggg",
"ThreeLinesLonggggggggggggg\nLongggggggggggggggg\nLongggggggggggggggg"
]
#State private var text: String = ""
var body: some View {
VStack {
Picker("Select Text", selection: self.$text) {
ForEach(self.items, id: \.self) {
Text($0)
.tag($0)
.fixedSize(horizontal: false, vertical: true)
}
}
Text("select: \(self.text)")
}
}
}
Here is the result:
Please tell me in which direction to look for the answer?
Please use .lineLimit(any number of max lines you want) with text.
In this case you can use
Text($0)
.tag($0)
.lineLimit(3)