struct MyNode : Identifiable {
var id: UUID
var weight: Double
init( weight: Double ) {
self.id = UUID()
self.weight = weight
}
}
struct MyNodeView : View {
#State var myNode : MyNode
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.weight))%").font(.footnote)
Slider(value: $myNode.weight, in: 0 ... 100)
}
}
}
Above is where I am creating the slider view so I can dynamically make a slider in a ForEach Loop. So I have a group of about 5 sliders and would like to save all of there values to an array. So [50, 60, 70, 80, 90] for each object. Is there a way of saving these values off to an array.
Group {
Text("Other defensive layers").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(defensiveLayers, id: \.self) { layer in
let node = MyNode(weight: 0)
HStack {
Text(layer).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
MyNodeView(myNode: node)
}
}
}
}.padding(.horizontal, 30)
}
Use an array of MyNode model and use #Binding.
Here is the demo code.
struct MyNodeView : View {
#Binding var myNode : MyNode
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.weight))%").font(.footnote)
Slider(value: $myNode.weight, in: 0 ... 100)
}
}
}
struct SwiftUIDEMO: View {
#State private var defensiveLayers: [MyNode] = (0..<5).map { _ -> MyNode in
MyNode(weight: 0)
}
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { index in
MyNodeView(myNode: $defensiveLayers[index])
}
}
}
}
Related
UPDATE: I've made this minimally reproducible.
I wish to make a grid for the alphabet, with each box the same size, looking like this:
I have a PreferenceKey, and a View extension, like this:
struct WidthPreference: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
extension View {
func sizePreference(letterIdx: Int) -> some View {
background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreference.self, value: proxy.size.width)
})
}
}
My primary view is an HStack nestled in a VStack, with each letter as a separate view. Here is the ContentView, and its Alphabet Grid:
struct ContentView: View {
#StateObject var theModel = MyModel()
var body: some View {
AlphabetGrid()
.textCase(.uppercase)
.font(.body)
.onAppear() {
theModel.initializeLetters()
}
.environmentObject(theModel)
}
}
struct AlphabetGrid: View {
#EnvironmentObject var theModel: MyModel
var spacing: CGFloat = 8
var body: some View {
let theKeyboard = [ theModel.allLetters?.filter { $0.keyboardRow == 0 },
theModel.allLetters?.filter { $0.keyboardRow == 1 },
theModel.allLetters?.filter { $0.keyboardRow == 2 }
]
VStack {
ForEach(theKeyboard, id: \.self) { keyboardRow in
HStack(alignment: .top) {
if let keyboardRow = keyboardRow {
ForEach(keyboardRow, id: \.self) { keyboardLetter in
let idx = keyboardLetter.letterStorePosition
LetterView(theIdx: idx, borderColour: .blue)
}
}
}
}
}
}
}
And then the Letter view, for each letter:
struct LetterView: View {
#EnvironmentObject var theModel: MyModel
var theIdx: Int
var borderColour: Color
var spacing: CGFloat = 8
#State private var cellWidth: CGFloat? = nil
func letterFor(letterIdx: Int) -> some View {
Text(String(theModel.allLetters?[letterIdx].letterStoreChar ?? "*"))
.sizePreference(letterIdx: letterIdx)
.frame(width: cellWidth, height: cellWidth, alignment: .center)
.padding(spacing)
}
var body: some View {
self.letterFor(letterIdx: theIdx)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColour, lineWidth: 1)
)
.onPreferenceChange(WidthPreference.self) { self.cellWidth = $0 }
}
}
Finally, for completeness, the Model to store the letters:
class MyModel: ObservableObject {
#Published var allLetters: [LetterData]?
struct LetterData: Hashable {
let letterStorePosition: Int
let letterStoreChar: Character
let keyboardRow: Int
let keyboardCol: Int
}
let keyboardWide = 9 // characters per row
// put all the alphabet characters into an array of LetterData elements
func initializeLetters() {
var tempLetters: [LetterData] = []
let allChars = Array("abcdefghijklmnopqrstuvwxyz")
for (index, element) in allChars.enumerated() {
let row = index / keyboardWide
let col = index % keyboardWide
tempLetters.append(LetterData(letterStorePosition: index, letterStoreChar: element,
keyboardRow: row, keyboardCol: col))
}
allLetters = tempLetters
}
}
Unfortunately, this makes a pretty, yet incorrect grid like this:
Any ideas on where I'm going wrong?
I did some digging, your PreferenceKey is being set with .background which just takes the size of the current View and you are using that value to turn into a square.
There is no match for the average just taking the current width and using it for the height.
extension View {
func sizePreference(letterIdx: Int) -> some View {
background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreference.self, value: proxy.size.width)
})
}
}
.frame(width: cellWidth, height: cellWidth, alignment: .center)
The width is based on the letter I being the most narrow and W being the widest.
Now, how to "fix" your code. You can move the onPreferenceChange up one View and use the min between the current cellWidth and the $0 instead of just replacing.
struct AlphabetGrid: View {
#EnvironmentObject var theModel: MyModel
#State private var cellWidth: CGFloat = .infinity
var spacing: CGFloat = 8
var body: some View {
let theKeyboard = [ theModel.allLetters?.filter { $0.keyboardRow == 0 },
theModel.allLetters?.filter { $0.keyboardRow == 1 },
theModel.allLetters?.filter { $0.keyboardRow == 2 }
]
VStack {
ForEach(theKeyboard, id: \.self) { keyboardRow in
HStack(alignment: .top) {
if let keyboardRow = keyboardRow {
ForEach(keyboardRow, id: \.self) { keyboardLetter in
let idx = keyboardLetter.letterStorePosition
LetterView(theIdx: idx, borderColour: .blue, cellWidth: $cellWidth)
}
}
}
}
} .onPreferenceChange(WidthPreference.self) { self.cellWidth = min(cellWidth, $0 ?? .infinity) }
}
}
Now with that fix you get a better looking keyboard but the M and W are cut off, to use the max you need a little more tweaking, ou can look at the code below.
import SwiftUI
class MyModel: ObservableObject {
#Published var allLetters: [LetterData]?
struct LetterData: Hashable {
let letterStorePosition: Int
let letterStoreChar: Character
let keyboardRow: Int
let keyboardCol: Int
}
let keyboardWide = 9 // characters per row
// put all the alphabet characters into an array of LetterData elements
func initializeLetters() {
var tempLetters: [LetterData] = []
let allChars = Array("abcdefghijklmnopqrstuvwxyz")
for (index, element) in allChars.enumerated() {
let row = index / keyboardWide
let col = index % keyboardWide
tempLetters.append(LetterData(letterStorePosition: index, letterStoreChar: element,
keyboardRow: row, keyboardCol: col))
}
allLetters = tempLetters
}
}
struct AlphabetParentView: View {
#StateObject var theModel = MyModel()
var body: some View {
AlphabetGrid()
.textCase(.uppercase)
.font(.body)
.onAppear() {
theModel.initializeLetters()
}
.environmentObject(theModel)
}
}
struct LetterView: View {
#EnvironmentObject var theModel: MyModel
var theIdx: Int
var borderColour: Color
var spacing: CGFloat = 8
#Binding var cellWidth: CGFloat?
func letterFor(letterIdx: Int) -> some View {
Text(String(theModel.allLetters?[letterIdx].letterStoreChar ?? "*"))
.padding(spacing)
}
var body: some View {
RoundedRectangle(cornerRadius: 8)
.stroke(borderColour, lineWidth: 1)
.overlay {
self.letterFor(letterIdx: theIdx)
}
.frame(width: cellWidth, height: cellWidth, alignment: .center)
}
}
struct AlphabetGrid: View {
#EnvironmentObject var theModel: MyModel
#State private var cellWidth: CGFloat? = nil
var spacing: CGFloat = 8
var body: some View {
let theKeyboard = [ theModel.allLetters?.filter { $0.keyboardRow == 0 },
theModel.allLetters?.filter { $0.keyboardRow == 1 },
theModel.allLetters?.filter { $0.keyboardRow == 2 }
]
VStack {
ForEach(theKeyboard, id: \.self) { keyboardRow in
HStack(alignment: .top) {
if let keyboardRow = keyboardRow {
ForEach(keyboardRow, id: \.self) { keyboardLetter in
let idx = keyboardLetter.letterStorePosition
LetterView(theIdx: idx, borderColour: .blue, cellWidth: $cellWidth)
.sizePreference()
}
}
}
}
} .onPreferenceChange(WidthPreference.self) {
if let w = cellWidth{
self.cellWidth = min(w, $0 ?? .infinity)
}else{
self.cellWidth = $0
}
}
}
}
struct AlphabetParentView_Previews: PreviewProvider {
static var previews: some View {
AlphabetParentView()
}
}
struct WidthPreference: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
extension View {
func sizePreference() -> some View {
background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreference.self, value: proxy.size.width)
})
}
}
There are simpler way of handling this like Ashley's example or SwiftUI.Layout and layout but this should help you understand why your squares were uneven.
Here's a fairly simple implementation, using a GeometryReader to allow us to calculate the width (and therefore the height), of each letter
struct ContentView: View {
let letters = ["ABCDEFGHI","JKLMNOPQR","STUVWXYZ"]
let spacing: CGFloat = 8
var body: some View {
GeometryReader { proxy in
VStack(spacing: spacing) {
ForEach(letters, id: \.self) { row in
HStack(spacing: spacing) {
ForEach(Array(row), id: \.self) { letter in
Text(String(letter))
.frame(width: letterWidth(for: proxy.size.width), height: letterWidth(for: proxy.size.width))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(.cyan, lineWidth: 1)
)
}
}
}
}
}
.padding()
}
func letterWidth(for width: CGFloat) -> CGFloat {
let count = CGFloat(letters.map(\.count).max()!)
return (width - (spacing * (count - 1))) / count
}
}
Hi I have a Stack of Hstacks that consist of a Text and a slider. The slider width extends to the edge of the text in front of it but I want them all to have the same width and appear in a straight column.
Like this below.
This is how I am forming the stacks.
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
this is the view where I am forming the sliders. Can someone pls help
struct MyNodeView : View {
#Binding var myNode : Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100)
}
}
}
To make sure all the text is the same width, the simplest way is .frame(width:).
struct Sliders {
var name = "Name"
var percent = CGFloat(100)
}
struct ContentView: View {
#State var defensiveLayers = [
Sliders(name: "Long name", percent: 80),
Sliders(name: "Name", percent: 70),
Sliders(name: "Ok name", percent: 65),
Sliders(name: "Hi", percent: 15),
Sliders(name: "Hello", percent: 45),
]
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
.frame(width: 100) /// add frame here!
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
}
}
struct MyNodeView : View {
#Binding var myNode: Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100)
}
}
}
Result:
You could calculate the required width for the longest Text and apply that particular width to all the leading Text views in all the HStacks unifying them in the width. I've created a helper method for finding the required width for any SwiftUI View.
Helper:
extension View {
func viewSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { }
}
Usage:
#State private var textFrameWidth: CGFloat = 0
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name)
.font(.custom("Gill Sans", size: 12))
.padding(.trailing)
.foregroundColor(.gray)
.viewSize { size in
textFrameWidth = textFrameWidth < size.width ? size.width : textFrameWidth
}
.frame(width: textFrameWidth)
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
}
I try to get( text & color ) from user and add them to the list in SwiftUI I already can pass text data but unfortunately for color I can't while they should be the same, below there is an image of app.To work we should provide a Binding for PreAddTextField .Thanks for your help
here is my Code :
import SwiftUI
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "", color: .purple)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id:
\.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id), colorPickerColor: <#Binding<Color>#>)
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false),appState: AppState())
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String
#Binding var colorPickerColor : Color
var body: some View {
HStack {
TextField("Enter text", text: $textInTextField)
ColorPicker("", selection: $colorPickerColor)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
DataModel :
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
var color : Color
}
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1", color: .purple),.init(text: "Item 2", color: .purple)]
}
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "", color: .purple)] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue, color: .purple)
}
}
}
}
and finaly :
import SwiftUI
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
HStack {
Image(systemName: "square")
.foregroundColor(text.color)
Text(text.text)
}
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Instead of using a Binding for just the String, you could create a Binding for the entire Text1 item:
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "", color: .purple)]
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
func bindingForId(id: UUID) -> Binding<Text1> { //now returns a Text1
.init { () -> Text1 in
self.textItemsToAdd.first(where: { $0.id == id }) ?? Text1(text: "", color: .clear)
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return newValue
}
}
}
}
struct PreAddTextField: View {
#Binding var item : Text1
var body: some View {
HStack {
TextField("Enter text", text: $item.text) //gets the text property of the binding
ColorPicker("", selection: $item.color) //gets the color property
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "", color: .purple)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in
PreAddTextField(item: viewModel.bindingForId(id: item.id)) //parameter is changed here
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
I'm trying to build an demo app by swiftUI that get multi text from user and add them to the list, below , there is an image of app every time user press plus button the AddListView show to the user and there user can add multi text to the List.I have a problem to add them to the list by new switUI data Flow I don't know how to pass data.(I comment more information)
Thanks 🙏
here is my code for AddListView:
import SwiftUI
struct AddListView: View {
#State var numberOfTextFiled = 1
#Binding var showAddListView : Bool
var body: some View {
ZStack {
Title(numberOfTextFiled: $numberOfTextFiled)
VStack {
ScrollView {
ForEach(0 ..< numberOfTextFiled, id: \.self) { item in
PreAddTextField()
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView)
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false))
}
}
struct PreAddTextField: View {
// I made this standalone struct and use #State to every TextField text be independent
// if i use #Binding to pass data all Texfield have the same text value
#State var textInTextField = ""
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
#Binding var numberOfTextFiled : Int
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
numberOfTextFiled += 1
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
and for DataModel:
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
var textData = [
Text1(text: "SwiftUI"),
Text1(text: "Data flow?"),
]
and finally:
import SwiftUI
struct ListView: View {
#State var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Because of the multiple-items part of the question, this becomes a lot less trivial. However, using a combination of ObservableObjects and callback functions, definitely doable. Look at the inline comments in the code for explanations about what is going on:
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
//Store the items in an ObservableObject instead of just in #State
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1"),.init(text: "Item 2")]
}
//This view model stores data about all of the new items that are going to be added
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "")] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue)
}
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "")) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String //this takes a binding to the view model now
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void //callback function for what happens when "Add" gets pressed
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void //callback function for what happens when the plus button is hit
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
I'm trying to create a List and allow only one item to be selected at a time. How would I do so in a ForEach loop? I can select multiple items just fine, but the end goal is to have only one checkmark in the selected item in the List. It may not even be the proper way to handle what I'm attempting.
struct ContentView: View {
var body: some View {
NavigationView {
List((1 ..< 4).indices, id: \.self) { index in
CheckmarkView(index: index)
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
//.environment(\.editMode, .constant(.active))
}
}
}
struct CheckmarkView: View {
let index: Int
#State var check: Bool = false
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
You'll need something to store all of the states instead of storing it per-checkmark view, because of the requirement to just have one thing checked at a time. I made a little example where the logic is handled in an ObservableObject and passed to the checkmark views through a custom Binding that handles checking/unchecking states:
struct CheckmarkModel {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.checkmarks[index].state
} set: { (newValue) in
self.checkmarks = self.checkmarks.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.state = newValue
} else {
//not the same index
if newValue {
itemCopy.state = false
}
}
return itemCopy
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
var body: some View {
NavigationView {
List(Array(state.checkmarks.enumerated()), id: \.1.id) { (index, item) in //<-- here
CheckmarkView(index: index + 1, check: state.singularBinding(forIndex: index))
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct CheckmarkView: View {
let index: Int
#Binding var check: Bool //<-- Here
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
What's happening:
There's a CheckmarkModel that has an ID for each checkbox, and the state of that box
StateManager keeps an array of those models. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the checkbox array. Any time a checkbox is set, it unchecks all of the other boxes. I also kept your original behavior of allowing nothing to be checked
The List now gets an enumeration of the state.checkmarks -- using enumerated lets me keep your previous behavior of being able to pass an index number to the checkbox view
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
List {
ForEach(0 ..< RemindTimeType.allCases.count) {
index in CheckmarkView(title:getListTitle(index), index: index, markIndex: $markIndex)
.padding(.all, 3)
}.listRowBackground(Color.clear)
}
struct CheckmarkView: View {
let title: String
let index: Int
#Binding var markIndex: Int
var body: some View {
Button(action: {
markIndex = index
}) {
HStack {
Text(title)
.foregroundColor(Color.white)
.font(.custom(FontEnum.Regular.fontName, size: 14))
Spacer()
if index == markIndex {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(hex: 0xe6c27c))
}
}
}
}
}
We can benefit from binding collections of Swift 5.5.
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
}
struct SingleSelectionList<Content: View>: View {
#Binding var items: [CheckmarkModel]
#Binding var selectedItem: CheckmarkModel?
var rowContent: (CheckmarkModel) -> Content
#State var previouslySelectedItemNdx: Int?
var body: some View {
List(Array($items.enumerated()), id: \.1.id) { (ndx, $item) in
rowContent(item)
.modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
.contentShape(Rectangle())
.onTapGesture {
if let prevIndex = previouslySelectedItemNdx {
items[prevIndex].state = false
}
self.selectedItem = item
item.state = true
previouslySelectedItemNdx = ndx
}
}
}
}
struct CheckmarkModifier: ViewModifier {
var checked: Bool = false
func body(content: Content) -> some View {
Group {
if checked {
ZStack(alignment: .trailing) {
content
Image(systemName: "checkmark")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.green)
.shadow(radius: 1)
}
} else {
content
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selectedItem: CheckmarkModel?
var body: some View {
VStack {
Text("Selected Item: \(selectedItem?.id.description ?? "Select one")")
Divider()
SingleSelectionList(items: $state.checkmarks, selectedItem: $selectedItem) { item in
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
}
}
}
}
}
A bit simplified version
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
var body: some View {
List {
ForEach($state.checkmarks) { $item in
SelectionCell(item: $item, selectedItem: $selection)
.onTapGesture {
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = false
}
selection = item.id
item.state = true
}
}
}
.listStyle(.plain)
}
}
struct SelectionCell: View {
#Binding var item: CheckmarkModel
#Binding var selectedItem: CheckmarkModel.ID?
var body: some View {
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
if item.id == selectedItem {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
A version that uses internal List's selected mark and selection:
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var name: String
var state: Bool = false
var id = UUID()
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(name: "Name1"), CheckmarkModel(name: "Name2"), CheckmarkModel(name: "Name3"), CheckmarkModel(name: "Name4")]
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
#State private var selectedItems = [CheckmarkModel]()
var body: some View {
VStack {
Text("Items")
List($state.checkmarks, selection: $selection) { $item in
Text(item.name + " " + item.state.description)
}
.onChange(of: selection) { s in
for index in state.checkmarks.indices {
if state.checkmarks[index].state == true {
state.checkmarks[index].state = false
}
}
selectedItems = []
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = true
selectedItems = [state.checkmarks[ndx]]
print(selectedItems)
}
}
.environment(\.editMode, .constant(.active))
Divider()
List(selectedItems) {
Text($0.name + " " + $0.state.description)
}
}
Text("\(selectedItems.count) selections")
}
}