I use a systemImage, with a onTapGesture function attached to it, to switch a boolean Variable to true. When that boolean variable is true, the view is changed. I positioned that systemImage at the top left part of the screen, using position(x:,y:) function. However, onTapGesture does not work when the value for "y" is bellow 100.
The code:
import SwiftUI
import FirebaseFirestoreSwift
import Firebase
struct ChatView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var homeData:HomeModel
#State var queryView:Bool = false
#EnvironmentObject var model:ContentModel
var user = Auth.auth().currentUser
let db = Firestore.firestore()
//If it is the first time when user scrolls
#State var scrolled = false
// #GestureState var isLongPressed = false
var body: some View {
if queryView == false {
VStack(spacing: 0) {
Text("\(homeData.query) Global Chat").foregroundColor(Color(#colorLiteral(red: 0.5951357484, green: 0.5694860816, blue: 1, alpha: 1))).font(.title3).padding(.top, 30)
Text("Welcome \(model.firstName) \(model.secondName) !").foregroundColor(Color(#colorLiteral(red: 0.5951357484, green: 0.5694860816, blue: 1, alpha: 1))).font(.callout)
Image(systemName: "arrow.backward.square")
.position(x: 30, y: 0)
.foregroundColor(Color(#colorLiteral(red: 0.5951357484, green: 0.5694860816, blue: 1, alpha: 1)))
.font(.system(size: 30, weight: .regular))
.onTapGesture {
withAnimation(.easeOut) {
queryView = true
}
print("TAPPED")
}
ScrollViewReader { reader in
ScrollView{
VStack(spacing: 15) {
ForEach(homeData.msgs) { msg in
ChatRow(chatData: msg, firstName: model.firstName, secondName: model.secondName).onAppear(){
if msg.id == self.homeData.msgs.last!.id && !scrolled {
reader.scrollTo(homeData.msgs.last!.id, anchor: .bottom)
scrolled = true
}
// print(model.firstName)
// print(model.secondName)
}
}.onChange(of: homeData.msgs) { value in
reader.scrollTo(homeData.msgs.last!.id, anchor: .bottom)
}
}
}.padding(.vertical)
}.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height - 135)
HStack(spacing:15) {
TextField("Enter message", text: $homeData.txt)
.padding(.horizontal)
//Fixed height for animation...
.frame(height: 45)
.foregroundColor(Color(.black))
.background(Color(#colorLiteral(red: 0.5951357484, green: 0.5694860816, blue: 1, alpha: 1)).opacity(1.0))
.clipShape(Capsule())
if homeData.txt != "" {
Button {
homeData.writeAllMessages()
} label: {
Image(systemName: "paperplane.fill")
.font(.system(size: 22))
.foregroundColor(.white)
.frame(width: 45, height: 45)
.background(Color(#colorLiteral(red: 0.5951357484, green: 0.5694860816, blue: 1, alpha: 1)))
.clipShape(Circle())
}
}
}.animation(.default)
.padding()
Spacer().frame(height: 100)
}.background(Color(.black).scaledToFill().frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height).ignoresSafeArea())
.navigationBarBackButtonHidden(true)
} else {
QueryView(query: homeData.query)
}
}
}
What shall I do in order to make that TapGesture work anywhere on the screen?
To provide more info,I use the systemImage with that tapgesture function because when I use the NavigationLink back button the transition to its parent view is too slow and laggy.
It's probably because the NavigationBar is still at the top of your View even though the Back button is hidden.
Try adding .navigationBarHidden(true) instead of .navigationBarBackButtonHidden(true).
Related
I am working on developing a quiz app. I have the following code, in two SwiftUI Views. Right now, the entire screen background color changes if you get an answer correct(to green)/incorrect(to red) but I want only the button background color to change, and the background of the screen to remain white. How do I implement this in the code?
Content View Swift:
import SwiftUI
struct ContentView: View {
let question = Question(questionText: "What was the first computer bug?", possibleAnswers: ["Ant", "Beetle", "Moth", "Fly"], correctAnswerIndex: 2)
#State var mainColor = Color(red: 255/255, green: 255/255, blue: 255/255)
#State var textBackgroundColor = Color.white
var body: some View {
ZStack {
mainColor.ignoresSafeArea()
VStack{
Text("Question 1 of 10")
.font(.callout)
.foregroundColor(.gray)
.padding(.bottom, 1)
.padding(.trailing, 170)
.multilineTextAlignment(.leading)
Text(question.questionText)
.font(.largeTitle)
.multilineTextAlignment(.leading)
.bold()
.background(textBackgroundColor)
Spacer()
.frame(height: 200)
VStack(spacing: 20){
ForEach(0..<question.possibleAnswers.count) { answerIndex in
Button(action: {
print("Tapped on option with the text: \(question.possibleAnswers[answerIndex])")
mainColor = answerIndex == question.correctAnswerIndex ? .green : .red
textBackgroundColor = answerIndex == question.correctAnswerIndex ? .green : .red
print(textBackgroundColor)
}, label: {
ChoiceTextView(choiceText: question.possibleAnswers[answerIndex])
})
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ChoiceTextView:
import SwiftUI
struct ChoiceTextView: View {
let choiceText: String
let accentColor = Color(red: 48/255, green: 105/255, blue: 240/255)
#State var textBackgroundColor = Color.white
var body: some View {
Text(choiceText)
.frame(width:250)
.foregroundColor(.black)
.padding()
// make background white to put shadow later
.background(textBackgroundColor)
.cornerRadius(10)
.shadow(color: Color(hue: 1.0, saturation: 0.0, brightness: 0.869), radius: 5, x: 0, y: 5)
}
}
struct ChoiceTextView_Previews: PreviewProvider {
static var previews: some View {
ChoiceTextView(choiceText: "Choice Text!", textBackgroundColor: Color.white)
}
}
Please help me figure this out!
I think this code does what you are trying to achieve.
Basically, avoid using the color to the main view's background and instead, put it in the button background modifier.
Also, instead of using the index of the array I use the value. I think it's easier to identify plus it avoids the ForEach warning that shows up for using a non fixed Int value.
struct Question {
let questionText: String
let possibleAnswers: [String]
let correctAnswer: String
}
struct ContentView: View {
let question = Question(
questionText: "What was the first computer bug?",
possibleAnswers: ["Ant", "Beetle", "Moth", "Fly"],
correctAnswer: "Beetle"
)
var body: some View {
ZStack {
VStack{
Text("Question 1 of 10")
.font(.callout)
.foregroundColor(.gray)
.padding(.bottom, 1)
.padding(.trailing, 170)
.multilineTextAlignment(.leading)
Text(question.questionText)
.font(.largeTitle)
.multilineTextAlignment(.leading)
.bold()
Spacer()
.frame(height: 200)
VStack(spacing: 20){
ForEach(question.possibleAnswers, id: \.self) { answer in
ChoiceButton(
answer: answer,
question: question
)
}
}
}
}
}
}
struct ChoiceButton: View {
let answer: String
let question: Question
#State private var mainColor = Color(red: 255/255, green: 255/255, blue: 255/255)
var body: some View {
Button {
print("Tapped on option with the text: \(answer)")
mainColor = answer == question.correctAnswer ? .green : .red
} label: {
Text(answer)
.frame(width:250)
.foregroundColor(.black)
.padding()
// make background white to put shadow later
.background(mainColor)
.cornerRadius(10)
.shadow(color: Color(hue: 1.0, saturation: 0.0, brightness: 0.869), radius: 5, x: 0, y: 5)
}
}
}
I have a list generated from a ForEach loop:
struct TrainingList: View {
#EnvironmentObject var trainingVM: TrainingViewModel
var body: some View {
VStack(alignment: .leading) {
Text("Your training sessions")
.font(.system(size: 35, weight: .semibold, design: .default))
.padding(.all, 10)
.foregroundColor(.white)
Divider()
ScrollView{
if(trainingVM.loading){
ProgressView("Loading training session").progressViewStyle(CircularProgressViewStyle(tint: .blue))
}
LazyVStack {
ForEach(trainingVM.trainingList) { training in
TrainingCell(training: training)
}
}
}
Spacer()
}
.background {
Rectangle()
.fill(Color(.sRGB, red: 41/255, green: 41/255, blue: 41/255))
.cornerRadius(10, corners: [.topRight, .bottomRight])
.shadow(color: .black.opacity(1), radius: 8, x: 6, y: 0)
}
.frame(width: 650)
.zIndex(.infinity)
}
}
Each TrainingCell has a button that opens an extra panel on the side of it. To indicate which row has the panel opened the button changes its styling:
struct TrainingCell: View {
#EnvironmentObject var trainingVM: TrainingViewModel
#State var showEvents = false
let training: Training
var body: some View {
HStack(spacing: 0) {
ZStack(alignment: .top) {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(.sRGB, red: 41/255, green: 41/255, blue: 41/255))
VStack {
HStack(spacing: 10) {
VStack(alignment: .leading, spacing: 5) {
HStack{
Text(training.readableDate, style: .date)
.font(.system(size: 25, weight: .semibold, design: .default))
.foregroundColor(.white)
Text(" | ")
Text(training.readableDate, style: .time)
.font(.system(size: 25, weight: .semibold, design: .default))
.foregroundColor(.white)
}
VStack(alignment: .leading,spacing: 5){
HStack {
HStack(spacing: 5) {
Image(systemName: "person.text.rectangle.fill")
.foregroundColor(Color(.sRGB, red: 10/255, green: 90/255, blue: 254/255))
Text(training.instructor.fullName)
.foregroundColor(.white)
}
}
HStack{
ForEach(training.students){ student in
HStack(spacing: 5) {
Image(systemName: "person")
.imageScale(.medium)
.foregroundColor(Color(.sRGB, red: 10/255, green: 90/255, blue: 254/255))
Text(student.fullName_shortenedFirstName)
.foregroundColor(.white)
}
}
}
}
.font(.system(size: 20, weight: .regular, design: .default))
.foregroundColor(.primary)
}
.frame(maxHeight: .infinity, alignment: .center)
.clipped()
Spacer()
View_Close_Button(showEvents: $showEvents)
}
.frame(maxWidth: .infinity, maxHeight: 80, alignment: .top)
.padding(.all)
.background {
RoundedRectangle(cornerRadius: 0, style: .continuous)
.fill(Color(.sRGB, red: 41/255, green: 44/255, blue: 49/255))
.shadow(color: .black.opacity(1), radius: 5, x: 0, y: 5)
}
}
}
}
}
}
The button code:
struct View_Close_Button: View {
#EnvironmentObject var trainingVM: TrainingViewModel
#Binding var showEvents: Bool
var body: some View {
HStack
{
Image(systemName: showEvents ? "xmark" : "eye")
.imageScale(.large)
.padding(.horizontal, 5)
.font(.system(size: 17, weight: .regular, design: .default))
Text(showEvents ? "Close" : "View")
.padding(.all, 10)
.font(.system(size: 25, weight: .regular, design: .default))
.multilineTextAlignment(.leading)
}
.onTapGesture {
withAnimation(.easeIn(duration: 0.3)) {
showEvents.toggle()
if(showEvents) {
trainingVM.showingEvents = true
}else{
trainingVM.showingEvents = false
}
}
}
.foregroundColor(.white)
.background {
Capsule(style: .continuous)
.foregroundColor(showEvents ? Color(.sRGB, red: 253/255, green: 77/255, blue: 77/255) : Color(.sRGB, red: 10/255, green: 90/255, blue: 254/255))
.clipped()
.frame(maxWidth: 180)
}
}
}
Which should result in this:
The only problem I have is that all button can be activated at the same time. How would I go about disabling the rest of the button when one is tapped?
I need the user to only be able to have on of the button displayed as "X Close"
I tought about looping trough other buttons to deactivate them programatically before activating the one that was tapped but I have no clue how
You could keep track of the activated button in the parent view.
If you have some kind of unique identifier per button you could make a variable in the parent view that contains the active identifier.
You can pass that variable as a binding into the button views and depending on that you can change the views appearance.
This way there is always just one active button. When a button is clicked you can set the value of the binding variable in the button view with the unique identifier of this button and the other views change automatically.
On the TrainingList you can define a variable with the active tab:
#State var activeTab: Int = 0
On TrainingCell you can add this variable as a binding.
#Binding var activeTab: Int
And you pass it like:
TrainingCell(training: training, activeTab: $activeTab)
Then on View_Close_Button you can add two variables:
#Binding var activeTab: Int
#State var training: Training
And pass it like this on the TrainingCell:
View_Close_Button(showEvents: $showEvents, activeTab: $activeTab, training: training)
In the View_Close_Button you can use this to get the value and set the styles accordingly:
Image(systemName: activeTab == training.id ? "xmark" : "eye")
Text(activeTab == training.id ? "Close" : "View")
And you can set it when the button it tapped:
.onTapGesture {
withAnimation(.easeIn(duration: 0.3) {
activeTab = training.id
}
}
I have 2 Integers: Xcode and Ycode. These are bindings from previous screens.
Now what I want is to present a new view based on these integers.
The app is a small quiz. So the Xcode and Ycode are the score.
But I want to present a new view when you click on the button "Click me" based on the Xcode and Ycode.
For example:
Value X = between 8-15 and value Y = between 8-23 -> present screen1
Value X = between 8-15 and value Y = between 24-40 -> present screen2
Value X = between 16-23 and value Y = between 8-17 -> present screen3
And so on......
This is my Code:
#Binding var Xcode: Int
#Binding var Ycode: Int
#State var ShowButton: Bool = false
#State var ButtonYes: Bool = false
#State var ButtonNo: Bool = false
#State var ButtonSometimes: Bool = false
var body: some View {
ZStack{
Image("Zebras")
.resizable()
.ignoresSafeArea()
.navigationBarHidden(true)
VStack{
Text("Wat ben ik?")
.font(.largeTitle)
.fontWeight(.heavy)
.padding()
.foregroundColor(.white)
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
Spacer()
Text("Je heb alle vragen beantwoord. Nu is de vraag: Welk dier ben ik?")
.foregroundColor(Color.white)
.font(.headline)
.padding()
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
Spacer()
Text("Your score:")
.foregroundColor(Color.white)
.font(.headline)
.padding()
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
HStack (spacing:0){
Text("X = ")
.foregroundColor(.white)
.font(.largeTitle)
.padding()
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
Text(String(Xcode))
.foregroundColor(.white)
.font(.largeTitle)
.padding()
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
}
HStack (spacing:0){
Text("Y = ")
.foregroundColor(.white)
.font(.largeTitle)
.padding()
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
Text(String(Ycode))
.foregroundColor(.white)
.font(.largeTitle)
.padding()
.background(Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
}
Spacer()
Button("Click here!") {
}
.frame(width: 100, height: 50, alignment: .center)
.font(.headline)
.foregroundColor(.white)
.padding()
.background(ButtonYes ? Color(red: 0.272, green: 0.471, blue: 0.262) : Color(red: 0.493, green: 0.184, blue: 0.487))
.cornerRadius(20)
.shadow(color: .black, radius: 10, x: 10, y: 10)
Spacer()
}
}
}
}
How could I create that?
you could use this approach, using a tuple, a switch and some NavigationLinks:
struct ContentView: View {
#State var xy = (5.0,7.0) // <-- use a tuple
var body: some View {
NavigationView {
QuizView(xy: $xy)
}.navigationViewStyle(.stack)
}
}
struct QuizView: View {
#Binding var xy: (Double, Double)
// set of ranges of your scores for each screen
let screen1X = 1.0..<4.0
let screen1Y = 2.0..<4.0
let screen2X = 3.0..<4.0
let screen2Y = 4.0..<8.0
let screen3X = 5.0..<9.0
let screen3Y = 6.0..<8.0
#State private var action: Int? = 0
var body: some View {
VStack {
Button(action: {
switch xy {
case (screen1X,screen1Y): action = 1
case (screen2X,screen2Y): action = 2
case (screen3X,screen3Y): action = 3
default:
print("---> default")
}
}) {
Text("Click me")
}
NavigationLink(destination: Text("screen1X"), tag: 1, selection: $action) {EmptyView()}
NavigationLink(destination: Text("screen2X"), tag: 2, selection: $action) {EmptyView()}
NavigationLink(destination: Text("screen3X"), tag: 3, selection: $action) {EmptyView()}
}
}
}
ContentView with 2 values: valueX and valueY. Then the ranges you need. Then a computed property to decide to which screen should navigate. The button is only created if the computed property doesn't return nil. And the destination of the navigation link has a switch which decides the screen to show and a label that is the button to be clicked.
import SwiftUI
struct MContentView: View {
#State var valueX = 17
#State var valueY = 15
#State var isNextViewActive = false
private let range8_15 = 8...15
private let range16_23 = 16...23
private let range8_23 = 8...23
private let range24_40 = 24...40
private let range8_17 = 8...17
private var screenToPresent: Int? {
if range8_15.contains(valueX) && range8_23.contains(valueY) {
return 1
} else if range8_15.contains(valueX) && range24_40.contains(valueY) {
return 2
} else if range16_23.contains(valueX) && range8_17.contains(valueY) {
return 3
}
return nil
}
var body: some View {
NavigationView {
if let screen = self.screenToPresent {
NavigationLink(isActive: self.$isNextViewActive, destination: {
switch screen {
case 1:
MView1()
case 2:
MView2()
case 3:
MView3()
default:
EmptyView()
}
}) {
Button(action: {
self.isNextViewActive = true
}) {
Text("Click me!")
}
}
}
}
}
}
struct MView1: View {
var body: some View {
Text("My View 1")
}
}
struct MView2: View {
var body: some View {
Text("My View 2")
}
}
struct MView3: View {
var body: some View {
Text("My View 3")
}
}
struct MExample_Previews: PreviewProvider {
static var previews: some View {
MContentView()
}
}
Hope is what you are looking for!
You could have hidden navigation links like these:
NavigationLink(destination: View1(), isActive: $condition1, label: { EmptyView() })
NavigationLink(destination: View2(), isActive: $condition2, label: { EmptyView() })
NavigationLink(destination: View3(), isActive: $condition3, label: { EmptyView() })
// note that having empty views for your links will keep them hidden on your layout
for each of your X and Y conditions.
So, when you check for X and Y values, you could verify them like this:
condition1 = Xcode >= 8 && Xcode <= 15 && Ycode >= 8 && Code <= 23
condition2 = Xcode >= 8 && Xcode <= 15 && Ycode >= 24 && Code <= 40
condition3 = Xcode >= 16 && Xcode <= 23 && Ycode >= 8 && Code <= 17
and that would activate the link you want and present the screen you need.
I can't seem to find the reason why there's a space between my list and navigation view. When I remove my navigation bar items, my list fills the space, when I have my navigation bar items, there's a gap between the navigation bar and my list. The style I'm using for my navigation bar is inline. Is this a bug in Xcode or is there something that I'm missing. Here's the code below.
...
import SwiftUI
struct DigitalReceiptContent: View {
#ObservedObject var store = ReceiptStore()
#State var showProfile = false
func addUpdate() {
store.updates.append(Update(name: "New Purchase", purchase: "$0.00", date: "00/00/21"))
}
var body: some View {
NavigationView {
List {
ForEach(store.updates) { update in
NavigationLink(destination: FullDigitalReceipt(update: update))
{
HStack {
Circle()
.frame(width: 50, height: 50)
.background(Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)))
.foregroundColor(.gray)
.cornerRadius(50)
.shadow(color: Color.black.opacity(0.6), radius: 2, x: 0, y: 1)
Text(update.name)
.font(.system(size: 20, weight: .bold))
.padding(.leading)
Spacer()
VStack(alignment: .trailing, spacing: 4.0) {
Text(update.date)
.font(.caption)
Text(update.purchase)
.foregroundColor(Color(#colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1)))
.fontWeight(.heavy)
}
}
.padding()
}
}
.onDelete{ index in
self.store.updates.remove(at: index.first!)
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Purchases"), displayMode: .inline)
.navigationBarItems(
// Selection button; organize purchases
leading: EditButton(),
trailing: Button(action: {self.showProfile.toggle() }) { Image(systemName: "person.crop.circle")
}
.sheet(isPresented: $showProfile) {
ProfileSettings()
})
}
}
}
...
I have 10 different choices in my view. I want to enable "Devam Et" button when any of 10 choices are made. It sounds easy but critical part is as following...When I click any of the first 8 buttons, I want to disable the last 2 buttons if they are selected and also if I select any of the last two options, I want to disable all the other 8 options if they are selected.
The code of the first 3 button configuration lines is as following...Remaining ones are same as these.
VStack{
HStack {
Button(action: {
self.tap1.toggle()
}) {
ZStack {
Rectangle()
.fill(self.tap1 ? Color(#colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)) : Color(#colorLiteral(red: 0.936548737, green: 0.936548737, blue: 0.936548737, alpha: 1)))
.frame(width: 25, height: 25)
if self.tap1 {
Image(systemName: "checkmark")
}
}.padding(.leading, 40)
}
Spacer()
Text("Diyabet")
.font(.system(size: 20, weight: .regular, design: .rounded))
.padding(.trailing, 200)
Spacer()
}.padding(.bottom, 10)
HStack {
Button(action: {
self.tap2.toggle()
}) {
ZStack {
Rectangle()
.fill(self.tap2 ? Color(#colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)) : Color(#colorLiteral(red: 0.936548737, green: 0.936548737, blue: 0.936548737, alpha: 1)))
.frame(width: 25, height: 25)
if self.tap2 {
Image(systemName: "checkmark")
}
}.padding(.leading, 40)
}
Spacer()
Text("YĆ¼ksek Tansiyon")
.font(.system(size: 20, weight: .regular, design: .rounded))
.padding(.trailing, 130)
Spacer()
}.padding(.bottom, 10)
HStack {
Button(action: {
self.tap3.toggle()
}) {
ZStack {
Rectangle()
.fill(self.tap3 ? Color(#colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)) : Color(#colorLiteral(red: 0.936548737, green: 0.936548737, blue: 0.936548737, alpha: 1)))
.frame(width: 25, height: 25)
if self.tap3 {
Image(systemName: "checkmark")
}
}.padding(.leading, 40)
}
button1
}
The code for "Devam Et" button is as following...
var button1: some View{
return Button(action: {
if self.tap1 == true || self.tap2 == true || self.tap3 == true || self.tap4 == true || self.tap5 == true || self.tap6 == true || self.tap7 == true || self.tap8 == true {
self.tap11.toggle()
}
else if self.tap9 == true {
self.tap11.toggle()
}
else if self.tap10 == true {
self.tap11.toggle()
}
}) {
Text("Devam Et")
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color.white)
.frame(width: 200, height: 30)
.padding()
.background(Color(#colorLiteral(red: 0.3101329505, green: 0.193462044, blue: 0.3823927939, alpha: 1)))
.cornerRadius(40)
.shadow(color: .gray, radius: 20.0, x: 20, y: 10)
.padding(.bottom, 70)
}.background(
NavigationLink(destination: destinationView, isActive: $tap11) {
EmptyView()
}
.hidden()
)
}
#ViewBuilder
var destinationView: some View {
if tap1 || tap2 || tap3 || tap4 || tap5 || tap6 || tap7 || tap8 || tap9 || tap10{
entrance5()
}
}
TL;DR: Result video: https://imgur.com/wRx2Ezg
Result output: https://imgur.com/H8fWwg0
Alright, this is a bit of a lengthy answer, before that I'll comment on some of your previous code:
It's not really a good choice to keep every boolean in their respective field, as you noticed it's really hard to keep track of them. Instead, you could have some sort of a struct that keeps track of every choice that you are providing.
What you also need is a proper state tracking, currently you need 3 type of state:
Standard state: Nothing has selected, yet.
Multiple choice state: There can be multiple selections, except the exclusive selections.
Exclusive choice state: Only one can be selected, in their respective group.
You also need to integrate it to your choices, to determine which kind you have selected in the list.
Last but not least, it should be handled outside of your current view, like in an interactor.
Here is the full code, ready to be tested in Playgrounds:
import SwiftUI
import PlaygroundSupport
enum ContentType {
case standard
case exclusive
}
enum ContentState {
case none
case multipleChoice
case exclusiveChoice
}
struct ContentChoice: Identifiable {
var id: String { title }
let title: String
let type: ContentType
var isSelected: Bool = false
var isDisabled: Bool = false
}
class ContentInteractor: ObservableObject, ContentChoiceViewDelegate {
#Published var choices: [ContentChoice] = []
#Published var state: ContentState = .none {
didSet {
print("state is now: \(state)")
switch state {
case .none:
exclusiveChoices.forEach { choices[$0].isDisabled = false }
standardChoices.forEach { choices[$0].isDisabled = false }
case .multipleChoice:
exclusiveChoices.forEach { choices[$0].isDisabled = true }
case .exclusiveChoice:
standardChoices.forEach { choices[$0].isDisabled = true }
}
}
}
private var exclusiveChoices: [Int] {
choices.indices.filter { choices[$0].type == .exclusive }
}
private var standardChoices: [Int] {
choices.indices.filter { choices[$0].type == .standard }
}
private var isExclusiveChoiceSelected: Bool {
choices.filter { $0.type == .standard && $0.isSelected }.count > 0
}
private var selectedMultipleChoiceCount: Int {
choices.filter { $0.type == .standard && $0.isSelected }.count
}
func didToggleChoice(_ choice: ContentChoice) {
guard let index = choices.firstIndex(where: { $0.id == choice.id }) else {
fatalError("No choice found with the given id.")
}
// This is where the whole algorithm lies.
switch state {
// Phase 1:
// If the user has not made any choice (state == .none),
// Enabling a `.standard` choice should lock the `.exclusive` choices.
// And vice versa.
case .none:
choices[index].isSelected.toggle()
switch choice.type {
case .standard:
state = .multipleChoice
case .exclusive:
state = .exclusiveChoice
}
// Phase 2:
// If the user is in multiple choice state,
// They can only select multiple choices. If any of the multiple choice
// is still selected, it should stay as is.
// If every choice is deselected, it should return the state to `.none`.
case .multipleChoice:
choices[index].isSelected.toggle()
switch choice.type {
case .standard:
if selectedMultipleChoiceCount == 0 {
state = .none
}
case .exclusive:
preconditionFailure("Unexpected choice selection.")
}
// Phase 3:
// If the user is in a not-answering state,
// They can only change it within themselves.
// If every choice is deselected, it should return the state to `.none`.
// Also, every exclusive choice is, exclusive.
// Hence, if one of them is selected, the others should be deselected.
case .exclusiveChoice:
switch choice.type {
case .standard:
preconditionFailure("Unexpected choice selection.")
case .exclusive:
let isSelecting = !choices[index].isSelected
if isSelecting {
exclusiveChoices.forEach { choices[$0].isSelected = false }
} else {
state = .none
}
}
choices[index].isSelected.toggle()
}
}
func didSelectSubmit() {
print("Current selection:", choices.filter { $0.isSelected }.map { $0.title })
}
}
protocol ContentChoiceViewDelegate: AnyObject {
func didToggleChoice(_ choice: ContentChoice)
func didSelectSubmit()
}
struct ContentChoiceView: View {
let choice: ContentChoice
weak var delegate: ContentChoiceViewDelegate!
init(choice: ContentChoice, delegate: ContentChoiceViewDelegate) {
self.choice = choice
self.delegate = delegate
}
var body: some View {
HStack {
Button(action: {
self.delegate.didToggleChoice(self.choice)
}) {
ZStack {
Rectangle()
.fill(self.choice.isSelected ? Color(#colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)) : Color(#colorLiteral(red: 0.936548737, green: 0.936548737, blue: 0.936548737, alpha: 1)))
.frame(width: 25, height: 25)
if self.choice.isSelected {
Image(systemName: "checkmark")
}
}.padding(.leading, 40)
}.disabled(self.choice.isDisabled)
Spacer()
Text(choice.title)
.font(.system(size: 20, weight: .regular, design: .rounded))
// .padding(.trailing, 200)
Spacer()
}.padding(.bottom, 10)
}
}
struct ContentView: View {
#ObservedObject var interactor: ContentInteractor
var body: some View {
VStack {
ForEach(interactor.choices) { choice in
ContentChoiceView(choice: choice, delegate: self.interactor)
}
Spacer()
submitButton
}.padding(.vertical, 70)
}
var submitButton: some View {
Button(action: {
self.interactor.didSelectSubmit()
}) {
Text("Devam Et")
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color.white)
.frame(width: 200, height: 30)
.padding()
.background(Color(#colorLiteral(red: 0.3101329505, green: 0.193462044, blue: 0.3823927939, alpha: 1)))
.cornerRadius(40)
.shadow(color: .gray, radius: 20.0, x: 20, y: 10)
}
}
}
let interactor = ContentInteractor()
["Diyabet", "Yuksek Tansiyon", "Astim"].forEach { title in
interactor.choices.append(ContentChoice(title: title, type: .standard))
}
["Soylememeyi Tercih Ederim", "Hicbirini Gecirmedim"].forEach { title in
interactor.choices.append(ContentChoice(title: title, type: .exclusive))
}
let contentView = ContentView(interactor: interactor)
PlaygroundPage.current.setLiveView(contentView)