I have a ForEach loop that displays ten unfilled buttons. When the button is clicked, the button assigns the index of the clicked button to a #State variable; only when the value of the state variable is equal to the index of the clicked button is the button filled. What should I change to be able to 1) click on the same button to unfill the button, and 2) fill another button without "unfilling" any already filled buttons?
Here is a minimal, reproducible example:
import SwiftUI
struct Test: View {
#State var selection: Int? = nil
var body: some View {
VStack {
ForEach (0 ..< 10, id: \.self) { number in
Button (action: {
self.selection = number
}, label: {
self.selection == number ? Image(systemName: "heart.fill") : Image(systemName: "heart")
})
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
Thanks in advance.
You need to have a selection property that can keep track of multiple buttons, not just one button. I would use a Set here (not an Array, because there's no handy remove-object method there)
struct Test: View {
#State var selection = Set<Int>() /// will contain the indices of all selected buttons
var body: some View {
VStack {
ForEach (0 ..< 10, id: \.self) { number in
Button(action: {
if selection.contains(number) {
selection.remove(number)
} else {
selection.insert(number)
}
}) {
selection.contains(number) ? Image(systemName: "heart.fill") : Image(systemName: "heart")
}
}
}
}
}
You have to maintain state separately for each button.Better option is to use array. Check code below.
import SwiftUI
struct Test: View {
#State var selection: [Int] = Array(repeating:-1,count:10)
var body: some View {
VStack {
ForEach (0..<selection.count, id: \.self) { index in
Button (action: {
if self.selection[index] == index {
self.selection[index] = -1
}else{
self.selection[index] = index
}
}, label: {
self.selection[index] == index ? Image(systemName: "heart.fill") : Image(systemName: "heart")
})
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
Related
this is my code! I want to complete this code and when I tapped on button the values of two selections start changing..
import SwiftUI
struct Container {
var one: String
var two: String
}
struct ContentView: View {
#State var container: Container
#State var list = ["1","2","3","4","5","6","7","8","9"]
var body: some View {
Picker("Select",selection: $container.one) {
ForEach(list, id: \.self) { item in
Text(item)
}
}
.pickerStyle(.menu)
Picker("Select",selection: $container.two) {
ForEach(list, id: \.self) { item in
Text(item)
}
}
.pickerStyle(.menu)
}
}
I found the solution with tuples
using this button :
Button {
(container.one , container.two) = (container.two , container.one)
}label: {
Image(systemName: "plus")
}
When using Popover with a button in SwiftUI, I want to popover with multiple buttons as shown below, but as it is, only the upper button
I can't get a popover. What if you want to popover both separately?
struct MySelection: Identifiable {
let id = UUID()
var text = ""
}
struct PopoverTest: View {
#State var selected: MySelection?
var body: some View {
VStack (spacing: 88) {
// First Button
Button(action: {
selected = MySelection(text: "Popover1")
}, label: {
Image(systemName: "stopwatch")
})
// Second Button
Button(action: {
selected = MySelection(text: "Popover2")
}, label: {
Image(systemName: "globe")
})
}
.buttonStyle(.plain)
.popover(item: $selected) { selection in
Text(selection.text).font(.largeTitle)
}
}
}
Thinking it over, the better way might be to create a custom "button with popover" view that can be used anywhere:
struct ContentView: View {
var body: some View {
VStack (spacing: 88) {
// First Button
ButtonWithPopover(image: "stopwatch",
item: MySelection(text: "Popover1"))
// Second Button
ButtonWithPopover(image: "globe",
item: MySelection(text: "Popover2"))
}
.buttonStyle(.plain)
}
}
struct ButtonWithPopover: View {
let image: String
let item: MySelection
#State var selected: MySelection?
var body: some View {
Button(action: {
selected = item
}, label: {
Image(systemName: image)
})
.popover(item: $selected) { selection in
Text(selection.text).font(.largeTitle)
}
}
}
In this case it seems you have to introduce 2 selected vars. The optional selected can hand over different values to the popover, but if it is triggered from different subviews, SwiftUI doesn't know where to anchor the popover to.
struct ContentView: View {
#State var selected1: MySelection?
#State var selected2: MySelection?
var body: some View {
VStack (spacing: 88) {
// First Button
Button(action: {
selected1 = MySelection(text: "Popover1")
}, label: {
Image(systemName: "stopwatch")
})
.popover(item: $selected1) { selection in
Text(selection.text).font(.largeTitle)
}
// Second Button
Button(action: {
selected2 = MySelection(text: "Popover2")
}, label: {
Image(systemName: "globe")
})
.popover(item: $selected2) { selection in
Text(selection.text).font(.largeTitle)
}
}
.buttonStyle(.plain)
}
}
I'm implementing Form and Picker with SwiftUI. There is a problem that it automatically navigates back to Form screen when I select a Picker option, how to keep it stay in selection screen?
Code:
struct ContentView: View {
#State private var selectedStrength = "Mild"
let strengths = ["Mild", "Medium", "Mature"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Strength", selection: $selectedStrength) {
ForEach(strengths, id: \.self) {
Text($0)
}
}
}
}
.navigationTitle("Select your cheese")
}
}
}
Actual:
Expect: (sample from Iphone Settings)
You may have to make a custom view that mimics what the picker looks like:
struct ContentView: View {
let strengths = ["Mild", "Medium", "Mature"]
#State private var selectedStrength = "Mild"
var body: some View {
NavigationView {
Form {
Section {
NavigationLink(destination: CheesePickerView(strengths: strengths, selectedStrength: $selectedStrength)) {
HStack {
Text("Strength")
Spacer()
Text(selectedStrength)
.foregroundColor(.gray)
}
}
}
}
.navigationTitle("Select your cheese")
}
}
}
struct CheesePickerView: View {
let strengths: [String]
#Binding var selectedStrength: String
var body: some View {
Form {
Section {
ForEach(0..<strengths.count){ index in
HStack {
Button(action: {
selectedStrength = strengths[index]
}) {
HStack{
Text(strengths[index])
.foregroundColor(.black)
Spacer()
if selectedStrength == strengths[index] {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}
I have an issue using a sheet inside a ForEach. Basically I have a List that shows many items in my array and an image that trigger the sheet. The problem is that when my sheet is presented it only shows the first item of my array which is "Harry Potter" in this case.
Here's the code
struct ContentView: View {
#State private var showingSheet = false
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.showingSheet = true
}
.sheet(isPresented: self.$showingSheet) {
Text(self.movies[movie])
}
}
}
}
}
}
There should be only one sheet, so here is possible approach - use another sheet modifier and activate it by selection
Tested with Xcode 12 / iOS 14 (iOS 13 compatible)
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
#State private var selectedMovie: Int? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
}
}
}
}
I changed your code to have only one sheet and have the selected movie in one variable.
extension String: Identifiable {
public var id: String { self }
}
struct ContentView: View {
#State private var selectedMovie: String? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(movies) { movie in
HStack {
Text(movie)
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie, content: { selectedMovie in
Text(selectedMovie)
})
}
}
}
Wanted to give my 2 cents on the matter.
I was encountering the same problem and Asperi's solution worked for me.
BUT - I also wanted to have a button on the sheet that dismisses the modal.
When you call a sheet with isPresented you pass a binding Bool and so you change it to false in order to dismiss.
What I did in the item case is I passed the item as a Binding. And in the sheet, I change that binding item to nil and that dismissed the sheet.
So for example in this case the code would be:
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
// My addition here: a "Done" button that dismisses the sheet
Button {
selectedMovie = nil
} label: {
Text("Done")
}
}
}
}
I want to create a grid of square buttons that I can click/tap to toggle between black and white.
As a half-way point I am creating a row of buttons that do this - see code below.
But when I click on one of them all the buttons toggle together.
I can't see why this is because I have a state variable for each Cell?
struct ContentView: View {
var body: some View {
ZStack {
Color.green
.edgesIgnoringSafeArea(.all)
RowOfCellsView(string: "X.X.X.X")
}
}
}
struct Cell: Identifiable {
var id: Int
var state: Bool
}
struct RowOfCellsView: View {
var string: String
var cells: [Cell] {
string.map { Cell(id: 1, state: $0 == ".") }
}
var body: some View {
HStack {
ForEach(cells) { cell in
CellView(isBlack: cell.state, symbol: "Q")
}
}
}
}
struct CellView: View {
#State var isBlack: Bool
#State var symbol: String
var body: some View {
Button(action: { self.isBlack.toggle() }) {
Text("")
.font(.largeTitle)
.frame(width: 40, height: 40)
.aspectRatio(1, contentMode: .fill)
}
.background(isBlack ? Color.black : Color.white)
}
}
Aha - just noticed that I have the id set to 1 for all Cells - that's the problem!
So needs to have a way to have a different id for each Cell.
For example could do:
string.enumerated().map { Cell(id: $0.0, state: $0.1 == ".") }