How to reset child view state variable with SwiftUI? - ios

I'm sure it's something very silly but how should one reset the state value of a child view when another state has changed?
For example, the code below shows 2 folders, which respectively have 2 and 3 items., which can be edited.
If you select the second folder (Work) and its 3rd item (Peter) and then select the first folder (Home), the app crashes since selectedItemIndex is out of bounds.
I tried to "reset" the state value when the view gets initialized but it seems like changing the state like such triggers out a "runtime: SwiftUI: Modifying state during view update, this will cause undefined behavior." warning.
init(items: Binding<[Item]>) {
self._items = items
self._selectedItemIndex = State(wrappedValue: 0)
}
What is the proper way to do this? Thanks!
Here's the code:
AppDelegate.swift
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let store = ItemStore()
let contentView = ContentView(store: store)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
ContentView.swift
import SwiftUI
final class ItemStore: ObservableObject {
#Published var data: [Folder] = [Folder(name: "Home",
items: [Item(name: "Mark"), Item(name: "Vincent")]),
Folder(name: "Work",
items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}
struct Folder: Identifiable {
var id = UUID()
var name: String
var items: [Item]
}
struct Item: Identifiable {
static func == (lhs: Item, rhs: Item) -> Bool {
return true
}
var id = UUID()
var name: String
var content = Date().description
init(name: String) {
self.name = name
}
}
struct ContentView: View {
#ObservedObject var store: ItemStore
#State var selectedFolderIndex: Int?
var body: some View {
HSplitView {
// FOLDERS
List(selection: $selectedFolderIndex) {
Section(header: Text("Groups")) {
ForEach(store.data.indexed(), id: \.1.id) { index, folder in
Text(folder.name).tag(index)
}
}.collapsible(false)
}
.listStyle(SidebarListStyle())
// ITEMS
if selectedFolderIndex != nil {
ItemsView(items: $store.data[selectedFolderIndex!].items)
}
}
.frame(minWidth: 800, maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ItemsView: View {
#Binding var items: [Item]
#State var selectedItemIndex: Int?
var body: some View {
HSplitView {
List(selection: $selectedItemIndex) {
ForEach(items.indexed(), id: \.1.id) { index, item in
Text(item.name).tag(index)
}
}
.frame(width: 300)
if selectedItemIndex != nil {
DetailView(item: $items[selectedItemIndex!])
.padding()
.frame(minWidth: 200, maxHeight: .infinity)
}
}
}
init(items: Binding<[Item]>) {
self._items = items
self._selectedItemIndex = State(wrappedValue: 0)
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
VStack {
TextField("", text: $item.name)
}
}
}
// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}

Thanks to #jordanpittman for suggesting a fix:
ItemsView(items: $store.data[selectedFolderIndex!].items).id(selectedRowIndex)
Source: https://swiftui-lab.com/swiftui-id

Fully playable sample draft for ContentView.swift. Play with it in both edit modes (inactive/active row selection) and adopt to your needs.
import SwiftUI
struct ItemStore {
var data: [Folder] = [Folder(name: "Home", items: [Item(name: "Mark"), Item(name: "Vincent")]),
Folder(name: "Work", items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}
struct Folder: Identifiable {
var id = UUID()
var name: String
var items: [Item]
}
struct Item: Identifiable {
var id = UUID()
var name: String
var content = Date().description
}
struct ContentView: View {
#State var store: ItemStore
#State var selectedFolderIndex: Int? = 0
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
VStack {
// FOLDERS
List(selection: $selectedFolderIndex) {
Section(header: Text("Groups")) {
ForEach(store.data.indexed(), id: \.1.id) { index, folder in
HStack {
Text(folder.name).tag(index)
Spacer()
}
.background(Color.white) //make the whole row tapable, not just the text
.frame(maxWidth: .infinity)
.multilineTextAlignment(.leading)
.onTapGesture {
self.selectedFolderIndex = index
}
}.onDelete(perform: delete)
}
}
.listStyle(GroupedListStyle())
.id(selectedFolderIndex)
// ITEMS
if selectedFolderIndex != nil && (($store.data.wrappedValue.startIndex..<$store.data.wrappedValue.endIndex).contains(selectedFolderIndex!) ){
ItemsView(items: $store.data[selectedFolderIndex!].items)
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing: EditButton())
.environment(\.editMode, $editMode)
}
}
func delete(at offsets: IndexSet) {
$store.wrappedValue.data.remove(atOffsets: offsets) // Note projected value! `store.data.remove() will not modify SwiftUI on changes and it will crash because of invalid index.
}
}
struct ItemsView: View {
#Binding var items: [Item]
#State var selectedDetailIndex: Int?
var body: some View {
HStack {
List(selection: $selectedDetailIndex) {
ForEach(items.indexed(), id: \.1.id) { index, item in
Text(item.name).tag(index)
.onTapGesture {
self.selectedDetailIndex = index
}
}
}
if selectedDetailIndex != nil && (($items.wrappedValue.startIndex..<$items.wrappedValue.endIndex).contains(selectedDetailIndex!) ) {
DetailView(item: $items[selectedDetailIndex!])
.padding()
}
}
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
VStack {
TextField("", text: $item.name)
}
}
}
// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(store: ItemStore())
}
}

Related

SwiftUI: Running into issues with Drag and Drop inside a LazyVGrid

I've been working on my own smart home app and have run into some issues when trying to build the grid for the app.
I've been basing this home app on this tutorial. The goal is that one can reorder the individually sized blocks in the grid basically like he or she wants. The blocks(items) represent different gadgets in the smart home application. The issue I'm facing is that I can't seem to get the drag & drop to work. Maybe it's better to put all the item views in one custom view and then run a "ForEach" loop for them so that the .onDrag works? I'm relatively new to SwiftUI so I appreciate every hint I can get this program to work.
Here is my code:
ItemModel1:
struct ItemModel: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
ItemModel2:
struct ItemModel2: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel2 {
return ItemModel2(id: id, title: title)
}
}
It's essentially the same for the two other ItemModels 3 and 4..
ItemViewModel:
class ItemViewModel {
var items: [ItemModel] = []
#Published var currentGrid: ItemModel?
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "Item1"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
ContentView:
struct DropViewDelegate: DropDelegate {
var grid: ItemModel
var gridData: ItemViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == gridData.currentGrid?.id
} ?? 0
let toIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == self.grid.id
} ?? 0
if fromIndex != toIndex{
withAnimation(.default){
let fromGrid = gridData.items[fromIndex]
gridData.items[fromIndex] = gridData.items[toIndex]
gridData.items[toIndex] = fromGrid
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
struct ContentView: View {
#State var items: [ItemModel] = []
#State var items2: [ItemModel2] = []
#State var items3: [ItemModel3] = []
#State var items4: [ItemModel4] = []
#State var gridData = ItemViewModel()
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
let columns2 = [
GridItem(.flexible()),
]
var body: some View {
ZStack{
ScrollView{
VStack{
HStack(alignment: .top){
Button(action: saveButtonPressed, label: {
Text("Item1")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed2, label: {
Text("Item2")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed3, label: {
Text("Item3")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed4, label: {
Text("Item4")
.font(.title2)
.foregroundColor(.white)
})
}
LazyVGrid(
columns: columns,
alignment: .leading,
spacing: 12
){
ForEach(items) { item in
Item1View (item: item)
if 1 == 1 { Color.clear }
}
ForEach(items4) { item4 in
Item4View (item4: item4)
if 1 == 1 { Color.clear }
}
ForEach(items2) { item2 in
Item2View (item2: item2)
}
LazyVGrid(
columns: columns2,
alignment: .leading,
spacing: 12
){
ForEach(items3) { item3 in
Item3View (item3: item3)
}
}
}
.onDrag({
self.gridData = items
return NSItemProvider(item: nil, typeIdentifier:
self.grid)
})
.onDrop(of: [items], delegate: DropViewDelegate(grid:
items, gridData: gridData))
}
}
}
}
func saveButtonPressed() {
addItem(title: "Hello")
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func saveButtonPressed2() {
addItem2(title: "Hello")
}
func addItem2(title: String) {
let newItem = ItemModel2(title: title)
items2.append(newItem)
}
func saveButtonPressed3() {
addItem3(title: "Hello")
}
func addItem3(title: String) {
let newItem = ItemModel3(title: title)
items3.append(newItem)
}
func saveButtonPressed4() {
addItem4(title: "Hello")
}
func addItem4(title: String) {
let newItem = ItemModel4(title: title)
items4.append(newItem)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
Item1:
struct Item1View: View {
#State var item: ItemModel
var body: some View {
HStack {
Text(item.title)
}
.padding( )
.frame(width: 396, height: 56)
.background(.black)
.cornerRadius(12.0)
}
}
Item2:
struct Item2View: View {
#State var item2: ItemModel2
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.black)
.cornerRadius(12.0)
}
}
Item3:
struct Item3View: View {
#State var item3: ItemModel3
var body: some View {
HStack {
Text(item3.title)
}
.padding( )
.frame(width: 182, height: 62)
.background(.black)
.cornerRadius(12.0)
}
}
Item4:
struct Item4View: View {
#State var item4: ItemModel4
var body: some View {
HStack {
Text(item4.title)
}
.padding( )
.frame(width: 396, height: 156)
.background(.black)
.cornerRadius(12.0)
}
}
I tried recreating the grid Asperi linked. However, the .onDrop doesn't seem to work like it should. The drop only occurs after you pressed another item to drag it. Only then will the previous items reorder themselves..
My version:
import SwiftUI
import UniformTypeIdentifiers
struct ItemModel6: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel6 {
return ItemModel6(id: id, title: title)
}
}
class Model: ObservableObject {
var data: [ItemModel6] = []
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
init() {
data = Array(repeating: ItemModel6(title: "title"), count:
100)
for i in 0..<data.count {
data[i] = ItemModel6(title: "Hello")
}
}
}
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: ItemModel6?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns) {
ForEach(model.data) { item2 in GridItemView (item2:
item2)
.overlay(dragging?.id == item2.id ?
Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = item2
return NSItemProvider(object:
String(item2.id) as NSString)
}
.onDrop(of: [UTType.text], delegate:
DragRelocateDelegate(item: item2, listData: $model.data,
current: $dragging))
}
}.animation(.default, value: model.data)
}
.onDrop(of: [UTType.text], delegate:
DropOutsideDelegate(current: $dragging))
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: ItemModel6?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DragRelocateDelegate: DropDelegate {
let item: ItemModel6
#Binding var listData: [ItemModel6]
#Binding var current: ItemModel6?
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
self.current = nil
return true
}
}
struct GridItemView: View {
#State var item2: ItemModel6
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.gray)
.cornerRadius(12.0)
}
}
struct DemoDragRelocateView_Previews: PreviewProvider {
static var previews: some View {
DemoDragRelocateView()
.preferredColorScheme(.dark)
}
}

SwiftUI Slide Over Animation Like Builtin Navigation

I'm experimenting with replicating SwiftUI's navigation without all the black box magic. However, I'm having trouble with the animation. No animation happens until maybe the second or third push/pop. When it does finally animate, it's hard to describe what it does. But it definitely isn't what I would expect.
I've tried various different animations but it's generally the same behavior.
struct RouterDemo: View {
#State private var items: [Int] = Array(0..<50)
#State private var selectedItem: Int?
var body: some View {
RouterStore(
route: $selectedItem,
state: { route in items.first(where: { $0 == route }) },
content: { ItemsList(items: items, selectedItem: $0) },
destination: { route, item in
ItemDetail(item: item, selectedItem: route)
}
)
}
}
public struct RouterStore<Destination, Content, Route, DestinationState>: View
where Destination: View,
Content: View,
Route: Hashable,
DestinationState: Equatable {
#Binding private var route: Route?
private let toDestinationState: (Route) -> DestinationState?
private let destination: (Binding<Route?>, DestinationState) -> Destination
private let content: (Binding<Route?>) -> Content
public init(
route: Binding<Route?>,
state toDestinationState: #escaping (Route) -> DestinationState?,
#ViewBuilder content: #escaping (Binding<Route?>) -> Content,
#ViewBuilder destination: #escaping (Binding<Route?>, DestinationState) -> Destination
) {
self._route = route
self.toDestinationState = toDestinationState
self.destination = destination
self.content = content
}
public var body: some View {
GeometryReader { geometry in
ZStack {
content($route)
wrappedDestination()
.frame(width: geometry.size.width)
.offset(
x: route == nil ? geometry.size.width : 0,
y: 0
)
.animation(self.animation)
}
}
}
private var animation: Animation = .easeIn(duration: 2)
#ViewBuilder
private func wrappedDestination() -> some View {
if let _route = Binding($route),
let _destinationState = toDestinationState(_route.wrappedValue) {
ZStack {
Group {
if #available(iOS 15.0, *) {
Color(uiColor: UIColor.systemBackground)
} else {
Color(UIColor.systemBackground)
}
}
.preferredColorScheme(.light)
.ignoresSafeArea()
self.destination($route, _destinationState)
}
} else {
EmptyView()
}
}
}
struct ItemsList: View {
let items: [Int]
#Binding var selectedItem: Int?
var body: some View {
List {
ForEach(items, id: \.self) { item in
Button(
action: { selectedItem = item },
label: { Text(String(item)) }
)
.contentShape(Rectangle())
}
}
}
}
struct ItemDetail: View {
let item: Int
#Binding var selectedItem: Int?
var body: some View {
VStack {
Text(String(item))
Button(
action: { selectedItem = nil },
label: { Text("Back") }
)
}
}
}
Thanks to the links Asperi provided, I figured it out.
Applying the animation to the container and providing the value to monitor to the animation fixed it.

Crash when array is empty in iOS 14.3

I'm trying to show some placeholder data when the array is empty. This works in iOS 13.7 but something has changed in iOS 14.3 so when the last item is deleted you get this crash:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
If I comment out testStore.data.isEmpty and just return the Form I get no crash.
How can I show placeholder when array is empty in iOS 14.3?
struct Test: Identifiable {
var text: String
var id: String { text }
}
extension Test {
final class Store: ObservableObject {
#Published var data = [Test(text: "Hi"), Test(text: "Bye")]
}
}
struct TestList: View {
#EnvironmentObject var testStore: Test.Store
var body: some View {
Group {
if testStore.data.isEmpty {
Text("Empty")
} else {
Form {
ForEach(testStore.data.indices, id: \.self) { index in
TestRow(test: $testStore.data[index], deleteHandler: { testStore.data.remove(at: index) })
}
}
}
}
}
}
struct TestRow: View {
#Binding var test: Test
let deleteHandler: (() -> ())
var body: some View {
HStack {
Text(test.text)
.font(.headline)
Spacer()
Button(action: deleteHandler, label: Image(systemName: "trash"))
}
}
}
You can use the extension proposed here:
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, #ViewBuilder content: #escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}
Then, if you also want to keep ForEach instead of List you can do:
struct TestList: View {
#EnvironmentObject var testStore: Test.Store
var body: some View {
Group {
if testStore.data.isEmpty {
Text("Empty")
} else {
Form {
ForEach(testStore.data.indices, id: \.self) { index in
Safe($testStore.data, index: index) { binding in
TestRow(test: binding, deleteHandler: { testStore.data.remove(at: index) })
}
}
}
}
}
}
}

SwiftUI: How to update passing array item in the other view

I'm trying to update arrays item with typed new value into Textfield, but List is not updated with edited value.
My Code is:
Model:
struct WalletItem: Identifiable{
let id = UUID()
var name:String
var cardNumber:String
var type:String
var cvc:String
let pin:String
var dateOfExpiry:String
}
ModelView:
class Wallet: ObservableObject{
#Published var wallets = [
WalletItem(name: "BSB", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2016-06-29"),
WalletItem(name: "Alpha bank", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2017-03-12"),
WalletItem(name: "MTŠ‘", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2020-11-12"),
]
}
First View:
struct WalletListView: View {
// Properties
// ==========
#ObservedObject var wallet = Wallet()
#State var isNewItemSheetIsVisible = false
var body: some View {
NavigationView {
List(wallet.wallets) { walletItem in
NavigationLink(destination: EditWalletItem(walletItem: walletItem)){
Text(walletItem.name)
}
}
.navigationBarTitle("Cards", displayMode: .inline)
.navigationBarItems(
leading: Button(action: { self.isNewItemSheetIsVisible = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add item")
}
}
)
}
.sheet(isPresented: $isNewItemSheetIsVisible) {
NewWalletItem(wallet: self.wallet)
}
}
}
and Secondary View:
struct EditWalletItem: View {
#State var walletItem: WalletItem
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form{
Section(header: Text("Card Name")){
TextField("", text: $walletItem.name)
}
}
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Back")
}, trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Save")
})
}
}
P.S: If I use #Binding instead of the #State I've got an error in the first view: Initializer init(_:) requires that Binding<String> conform to StringProtocol
Here are modified parts (tested & works with Xcode 11.2 / iOS 13.2):
Sure over binding
struct EditWalletItem: View {
#Binding var walletItem: WalletItem
Place to pass it
List(Array(wallet.wallets.enumerated()), id: .element.id) { (i, walletItem) in
NavigationLink(destination: EditWalletItem(walletItem: self.$wallet.wallets[i])){
Text(walletItem.name)
}
}
ForEach(Array(list.enumerated())) will only work correctly if the list is an Array but not for an ArraySlice, and it has the downside of copying the list.
A better approach is using a .indexed() helper:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { self.base.startIndex }
var endIndex: Index { self.base.endIndex }
func index(after i: Index) -> Index {
self.base.index(after: i)
}
func index(before i: Index) -> Index {
self.base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
self.base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: self.base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Example:
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Foundation
import SwiftUI
struct Position {
var id = UUID()
var count: Int
var name: String
}
class BookingModel: ObservableObject {
#Published var positions: [Position]
init(positions: [Position] = []) {
self.positions = positions
}
}
struct EditableListExample: View {
#ObservedObject var bookingModel = BookingModel(
positions: [
Position(count: 1, name: "Candy"),
Position(count: 0, name: "Bread"),
]
)
var body: some View {
// >>> Passing a binding into an Array via index:
List(bookingModel.positions.indexed(), id: \.element.id) { i, _ in
PositionRowView(position: self.$bookingModel.positions[i])
}
}
}
struct PositionRowView: View {
#Binding var position: Position
var body: some View {
Stepper(
value: $position.count,
label: {
Text("\(position.count)x \(position.name)")
}
)
}
}
struct EditableListExample_Previews: PreviewProvider {
static var previews: some View {
EditableListExample()
}
}
See also:
How does the Apple-suggested .indexed() property work in a ForEach?

SwiftUI Picker in a Form doesn't show the selected row

I am trying to have a Picker that shows which option is currently selected.
Try out the following code which correctly selects the right option but the picker does not show which option is selected:
import SwiftUI
struct ContentView: View {
#State var selectedIndex: Int = 0
let strings: [String] = {
var strings: [String] = []
for i in 0..<10 {
strings.append("\(i)")
}
return strings
}()
var body: some View {
NavigationView {
VStack {
Form {
Picker(selection: $selectedIndex,
label: Text("Selected string: \(strings[selectedIndex])")) {
ForEach(0..<strings.count) {
Text(self.strings[$0]).tag($0)
}
}
}
}
.navigationBarTitle("Form Picker",
displayMode: NavigationBarItem.TitleDisplayMode.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone know what could be wrong? It's observed using Xcode 11.1 and iOS 13.1
I created the simple picker I call "ListPicker" which should fit the bill. I've written it so it works well in a Form; if you need it outside of a Form you will have to tinker with it. If you see any way to improve the code, please add a comment; this is still a learning experience for all of us.
// MARK: - LIST PICKER (PUBLIC)
struct ListPicker<Content: View>: View {
#Binding var selectedItem: Int
var label: () -> Content
var data: [Any]
var selectedLabel: String {
selectedItem >= 0 ? "\(data[selectedItem])" : ""
}
var body: some View {
NavigationLink(destination: ListPickerContent(selectedItem: self.$selectedItem, data: self.data)) {
ListPickerLabel(label: self.label, value: "\(self.selectedLabel)")
}
}
}
// MARK: - INTERNAL
private struct ListPickerLabel<Content: View>: View {
let label: () -> Content
let value: String
var body: some View {
HStack(alignment: .center) {
self.label()
Spacer()
Text(value)
.padding(.leading, 8)
}
}
}
private struct ListPickerContentItem: View {
let label: String
let index: Int
let isSelected: Bool
var body: some View {
HStack {
Text(label)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}.background(Color.white) // so the entire row is selectable
}
}
private struct ListPickerContent: View {
#Environment(\.presentationMode) var presentationMode
#Binding var selectedItem: Int
var data: [Any]
var body: some View {
List {
ForEach(0..<data.count) { index in
ListPickerContentItem(label: "\(self.data[index])", index: index, isSelected: index == self.selectedItem).onTapGesture {
self.selectedItem = index
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
Then you can use it like this:
#State var selectedCar: Int = 0
let cars = ["Jaguar", "Audi", "BMW", "Land Rover"]
Form {
ListPicker(
selectedItem: self.$selectedCar,
label: {
Text("Cars")
},
data: self.cars
)
}

Resources