Selection in Sectioned List in SwiftUI - swift

If I have a list with multiple sections of heterogenous type (say like a sidebar in a macOS app), how do I keep track of the $selection? Here's a simplified example :
struct A: Hashable {
let propA: Int
}
struct B: Hashable {
let propB: Int
}
struct ContentView: View {
let allAs = (1...10).map{A(propA: $0)}
let allBs = (1...5).map{B(propB: $0)}
#Binding var selectedA: A?
#Binding var selectedB: B?
var body: some View {
List() { // What goes in selected: ?
Section() {
ForEach(allAs, id: \.propA) {
Text(String($0.propA))
}
} header: {
Text("A Section")
}
Section() {
ForEach(allBs, id: \.propB) {
Text(String($0.propB))
}
} header: {
Text("B Section")
}
}
}
}
Is the only way around this to create a generic type or protocol to hold A and B?

Related

Using protocol to capture SwiftUI view

I’m trying to write a protocol that requires conformers to have a View property, or a #ViewBuilder method that returns some View.
I want to have a re-useable composite view that can build different sub views based on what type of data needs to be displayed.
The protocol would look like this…
protocol RowView {
var leftSide: some View { get }
var rightSide: some View { get }
}
That way I could call something like this…
struct Example: RowView {
var id: Int
var leftSide: some View { … }
var rightSide: some View { … }
}
struct ContentView: View {
let rows: [RowView]
var body: some View {
VStack {
Foreach(rows, id: \.id) {
HStack {
$0.leftSide
$0.rightSide
}
}
}
}
}
You need to change the protocol to something like:
protocol RowView {
associatedtype LView: View
associatedtype RView: View
var leftSide: LView { get }
var rightSide: RView { get }
}
Also, use the concrete Example type in the content view instead of the protocol (the protocol you defined doesn't have id at all):
let rows: [Example]
Also! you can make the RowView to be identifiable as your need, So no need for id: \.id anymore:
protocol RowView: Identifiable
A working code:
protocol RowView: Identifiable {
associatedtype LView: View
associatedtype RView: View
var leftSide: LView { get }
var rightSide: RView { get }
}
struct Example: RowView {
var id: Int
var leftSide: some View { Text("Left") }
var rightSide: some View { Text("Right") }
}
struct ContentView: View {
let rows: [Example] = [
.init(id: 1),
.init(id: 2)
]
var body: some View {
VStack {
ForEach(rows) { row in
HStack {
row.leftSide
row.rightSide
}
}
}
}
}

swift picker not selecting item

I have a short array of items that I want to display in a segmented picker. I'm passing the selected item (0, by default). The picker displays, but no item is selected, and the picker is unresponsive to clicks (in the simulator). I have a very similar picker that uses percentage values, and it works correctly. I am guessing that the issue has to do with the closure that I'm passing to the ForEach loop, but I am unclear on what syntax I should be using, if that is in fact the issue.
The code is as follows:
#State private var originalUnit = 0
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection $originalUnit) {
ForEach(sourceUnits, id: \.self {
Text($0)
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any insights on this would be appreciated. Thanks in advance!
You have a type mismatch between your originalUnit (Int) and your sourceUnits (String). Your selection needs to match the type.
struct ContentView: View {
#State private var originalUnit = "meters" //<-- Here
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection: $originalUnit) {
ForEach(sourceUnits, id: \.self) {
Text($0)
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
If, for some reason, you really needed originalUnit to be an Int, you could use enumerated (normally not the most efficient method for large collections in a ForEach, but that'll be inconsequential for something this small) so that the id is the index and matches the type (Int) of originalUnit:
struct ContentView: View {
#State private var originalUnit = 0
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection: $originalUnit) {
ForEach(Array(sourceUnits.enumerated()), id: \.0) { //<-- .0 is the index (Int)
Text($0.1) //<-- .1 is the original item String
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
You can set tag for your picker's row. Tag is any hashable type.
Refer this code work with any type of object for selection
struct TestView: View {
#StateObject var viewModel = TestViewModel()
var body: some View {
VStack {
Text(viewModel.selectedItem.title)
Picker("Select item", selection: $viewModel.selectedItem) {
ForEach(viewModel.items) { makeRowForItem($0) }
}
}
}
#ViewBuilder
func makeRowForItem(_ item: Item) -> some View {
Text(item.title).tag(item)
}
}
struct Item: Identifiable, Hashable {
var id = UUID().uuidString
var title = "Untitled"
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
class TestViewModel: ObservableObject {
#Published var selectedItem: Item
#Published var items: [Item]
init() {
let list = (1..<10).map { Item(title: "Untitled \($0)") }
items = list
selectedItem = list.first!
}
}

I want to create a loop that creates 10 different strings and use that strings to show 10 different cards

import Foundation
class Card: Identifiable, ObservableObject {
var id = UUID()
var result: String = ""
init() {
for _ in 1...10 {
let suit = ["D","K","B","S"][Int(arc4random() % 4]
let numStr = ["1","2","3","4"][Int(arc4random() % 4]
result = suit + numStr
}
}
}
import SwiftUI
struct EasyModeView: View {
#ObservedObject var cardModel: Card
var body : some View {
HStack {
ForEach((1...10, id: \.self) { _ in
Button { } label: { Image(cardModel.result) } } }
}
}
I created loop but it always shows me 10 same cards and i want all 10 different.
My Images in Assets are named same as combination of two variables example "D1","D2","S1","S2" etc.
Here is a right way of what you are trying:
struct ContentView: View {
var body: some View {
EasyModeView()
}
}
struct EasyModeView: View {
#StateObject var cardModel: Card = Card()
var body : some View {
HStack {
ForEach(cardModel.results) { card in
Button(action: {}, label: {
Text(card.result)
.padding(3.0)
.background(Color.yellow.cornerRadius(3.0))
})
}
}
Button("re-set") { cardModel.reset() }.padding()
}
}
struct CardType: Identifiable {
let id: UUID = UUID()
var result: String
}
class Card: ObservableObject {
#Published var results: [CardType] = [CardType]()
private let suit: [String] = ["D","K","B","S"]
init() { initializing() }
private func initializing() {
if (results.count > 0) { results.removeAll() }
for _ in 0...9 { results.append(CardType(result: suit.randomElement()! + String(describing: Int.random(in: 1...4)))) }
}
func reset() { initializing() }
}
Result:
You are using only one card, instead of creating 10. Fix like this
struct EasyModeView: View {
var body : some View {
HStack {
ForEach(0..<10) { _ in
Button { } label: {
Image(Card().result)
}
}
}
}
}

Require a SwitftUI View in a protocol without boilerplate

[ Ed: Once I had worked this out, I edited the title of this question to better reflect what I actually needed. - it wasn't until I answered my own question that I clarified what I needed :-) ]
I am developing an App using SwiftUI on IOS in which I have 6 situations where I will have a List of items which I can select and in all cases the action will be to move to a screen showing that Item.
I am a keen "DRY" advocate so rather than write the List Code 6 times I want to abstract away the list and select code and for each of the 6 scenarios I want to just provide what is unique to that instance.
I want to use a protocol but want to keep boilerplate to a minimum.
My protocol and associated support is this:
import SwiftUI
/// -----------------------------------------------------------------
/// ListAndSelect
/// -----------------------------------------------------------------
protocol ListAndSelectItem: Identifiable {
var name: String { get set }
var value: Int { get set }
// For listView:
static var listTitle: String { get }
associatedtype ItemListView: View
func itemListView() -> ItemListView
// For detailView:
var detailTitle: String { get }
associatedtype DetailView: View
func detailView() -> DetailView
}
extension Array where Element: ListAndSelectItem {
func listAndSelect() -> some View {
return ListView(items: self, itemName: Element.listTitle)
}
}
struct ListView<Item: ListAndSelectItem>: View {
var items: [Item]
var itemName: String
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(
destination: DetailView(item: item, index: String(item.value))
) {
VStack(alignment: .leading){
item.itemListView()
.font(.system(size: 15)) // Feasible that we should remove this
}
}
}
.navigationBarTitle(Text(itemName).foregroundColor(Color.black))
}
}
}
struct DetailView<Item: ListAndSelectItem>: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var item: Item
var index: String
var body: some View {
NavigationView(){
item.detailView()
}
.navigationBarTitle(Text(item.name).foregroundColor(Color.black))
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("<").foregroundColor(Color.black)}))
}
}
which means I can then just write:
struct Person: ListAndSelectItem {
var id = UUID()
var name: String
var value: Int
typealias ItemListView = PersonListView
static var listTitle = "People"
func itemListView() -> PersonListView {
PersonListView(person: self)
}
typealias DetailView = PersonDetailView
let detailTitle = "Detail Title"
func detailView() -> DetailView {
PersonDetailView(person: self)
}
}
struct PersonListView: View {
var person: Person
var body: some View {
Text("List View for \(person.name)")
}
}
struct PersonDetailView: View {
var person: Person
var body: some View {
Text("Detail View for \(person.name)")
}
}
struct ContentView: View {
let persons: [Person] = [
Person(name: "Jane", value: 1),
Person(name: "John", value: 2),
Person(name: "Jemima", value: 3),
]
var body: some View {
persons.listAndSelect()
}
}
which isn't bad but I feel I ought to be able to go further.
Having to write:
typealias ItemListView = PersonListView
static var listTitle = "People"
func itemListView() -> PersonListView {
PersonListView(person: self)
}
with
struct PersonListView: View {
var person: Person
var body: some View {
Text("List View for \(person.name)")
}
}
still seems cumbersome to me.
In each of my 6 cases I'd be writing very similar code.
I feel like I ought to be able to just write:
static var listTitle = "People"
func itemListView() = {
Text("List View for \(name)")
}
}
because that's the unique bit.
But that certainly won't compile.
And then the same for the Detail.
I can't get my head around how to simplify further.
Any ideas welcome?
The key to this is, if you want to use a view in a protocol then:
1) In the protocol:
associatedtype SpecialView: View
var specialView: SpecialView { get }
2) In the struct using the protocol:
var specialView: some View { Text("Special View") }
So in the situation of the question:
By changing my protocol to:
protocol ListAndSelectItem: Identifiable {
var name: String { get set }
var value: Int { get set }
// For listView:
static var listTitle: String { get }
associatedtype ListView: View
var listView: ListView { get }
// For detailView:
var detailTitle: String { get }
associatedtype DetailView: View
var detailView: DetailView { get }
}
I can now define Person as:
struct Person: ListAndSelectItem {
var id = UUID()
var name: String
var value: Int
static var listTitle = "People"
var listView: some View { Text("List View for \(name)") }
var detailTitle = "Person"
var detailView: some View { Text("Detail View for \(name)") }
}
which is suitable DRY and free of boilerplate!

What is the correct way to create SwiftUI Binding with array of associative enum?

I got some (unexplained) crashes earlier today, and simplified my code to what is seen below. The crashing went away, but I am not 100% sure. Is the code below the correct way to create Binding on an array of enums? And if yes, can this code be made simpler?
import SwiftUI
enum TheEnum: Hashable {
case one(Int), two(Float)
}
class TestModel : ObservableObject {
#Published var enumArray = [TheEnum.one(5), TheEnum.two(6.0)]
}
struct ContentView: View {
#ObservedObject var testModel = TestModel()
var body: some View {
HStack {
ForEach(testModel.enumArray, id: \.self) { value -> AnyView in
switch value {
case .one(var intVal):
let b = Binding(get: {
intVal
}) {
intVal = $0
}
return AnyView(IntView(intVal: b))
case .two(var floatVal):
let b = Binding(get: {
floatVal
}) {
floatVal = $0
}
return AnyView(FloatView(floatVal: b))
}
}
}
}
}
struct IntView: View {
#Binding var intVal: Int
var body: some View {
Text("\(intVal)")
}
}
struct FloatView: View {
#Binding var floatVal: Float
var body: some View {
Text("\(floatVal)")
}
}