Multiple List with SwiftUI - ios

Let say That I have an Array of arrays like so:
var arrays = [["one", "two", "three", "Four"], ["four", "five", "six"]]
My goal is to create a list for each child of each subarray then navigate to the next child.
Example 1: one -> (NavigationLink) two -> (NavigationLink) three -> (NavigationLink) Four.
Example 2: four -> (NavigationLink) five -> (NavigationLink) six.
Each arrow is a NavigationLink between each List.
Here's what I've tried so far:
struct ContentView: View {
let arrays = [["A", "B", "C", "D"], ["E", "F", "G", "H"]]
var body: some View {
NavigationView {
List {
ForEach(arrays, id: \.self) { item in
NavigationLink( destination: DestinationView(items: item)) {
Text(item.first!).font(.subheadline)
}
}
}
.navigationBarTitle("Dash")
}
}
}
struct DestinationView : View {
var items: [String]
var body: some View {
List {
ForEach(items, id: \.self) { item in
NavigationLink( destination: DestinationView(items: items)) {
Text(item).font(.subheadline)
}
}
}
}
}

You can do it like this by slicing the array each time you pass it to the DestinationView
import SwiftUI
struct ContentView: View {
let arrays = [["A", "B", "C", "D"], ["E", "F", "G", "H"]]
var body: some View {
NavigationView {
List {
ForEach(arrays, id: \.self) { item in
NavigationLink( destination: DestinationView(items: Array(item[1..<item.count]))) {
Text(item.first!).font(.subheadline)
}
}
}
.navigationBarTitle("Dash")
}
}
}
struct DestinationView : View {
var items: [String]
var body: some View {
List {
if self.items.count > 0 {
NavigationLink( destination: DestinationView(items: Array(items[1..<items.count]))) {
Text(items[0]).font(.subheadline)
}
}
}
}
}

Related

SwiftUI : How can I change the index from a view and show the correct Text in another view?

I don't understand why my DetailView is showing the same Text. (gif below)
Indeed I implemented a method selectTheme in ContentView that should change the selectedIndex according to the index of the row when the DetailView Appears but it seems that it does not take into account the var selectedIndex in my ViewModel remains 0.
Do you have any idea why? I really don't understand what going wrong here.
Thank you.
struct ContentView: View {
var vm:ViewModel
var body: some View {
NavigationView{
List{
ForEach(vm.sentences, id:\.self) { indexNumb in
NavigationLink(destination: DetailView(vm: vm).onAppear(perform: {
vm.selectTheme(sentence: indexNumb)
print(vm.groupedItems)
print("Array :\(vm.sylbArray)")
})) {
Text(String(indexNumb))
}
}
}
}
}
}
struct DetailView: View {
var vm:ViewModel
var body: some View {
ForEach(vm.groupedItems,id:\.self) { subItems in
HStack{
ForEach(subItems,id:\.self) { word in
Button(action: {
print(vm.groupedItems)
print(vm.selectedIndex)
}, label: {
Text(word)
})
}
}
}
}
}
class ViewModel{
#Published var sylbArray: [String] = []
var groupedItems: [[String]] = []
init() {
appendArray(string: String(sentences[selectedIndex]))
groupedItems = [sylbArray]
}
var sentences = [13,4]
func appendArray(string: String) {
sylbArray.append(string)
}
// remains 0 : why ?
var selectedIndex = 0
func selectTheme(sentence: Int) {
if let index = sentences.firstIndex(of: sentence) {
selectedIndex = index
}
}
}
Your viewModel must conform to class protocol 'ObservableObject'
class ViewModel: ObservableObject {
var sentences = [13, 4]
#Published var selectedSentens: Int?
}
here is an example of what you want to do.
struct ContentView: View {
#StateObject var vm: ViewModel = .init()
var body: some View {
NavigationSplitView {
List(vm.items, selection: $vm.selectedItem) { item in
Text(item.name)
.tag(item)
}
} detail: {
if let item = vm.selectedItem{
DetailView(for: item)
}
}
.navigationSplitViewStyle(.balanced)
}
}
item object:
struct Item: Hashable, Identifiable {
var name: String
var subItems: [String]
var id = UUID()
}
viewModel class
class ViewModel: ObservableObject {
var items: [Item] = [
Item(name: "Item 1", subItems: [ "sub 11", "sub 12" ] ),
Item(name: "Item 2", subItems: [ "sub 21", "sub 22" ] )
]
#Published var selectedItem: Item?
}
DetailView:
struct DetailView: View {
var item: Item
init(for item: Item) {
self.item = item
}
var body: some View {
List(item.subItems, id: \.self){ subItem in
Text(subItem)
}
}
}

How to Implement Dynamic SwiftUI View Hierarchy?

I'm trying to implement a markdown renderer using SwiftUI. Markdown document contains a variety of blocks, where a block may be embedded in another block, for example, block quotes:
Quote Level 1
Quote Level 2
Quote Level 3
...
The entire document forms a tree-like structure with arbitrary depth, requiring the renderer to take a recursive approach. I adopted the following code structure:
#ViewBuilder
func renderBlock(block: Block) -> some View {
switch block {
// other types of elements
case let block as BlockQuote:
HStack {
GrayRectangle()
ForEach(block.children) { child in
renderBlock(child) // recursion
}
}
}
}
However, the compiler rejects that as it require the return type to be determined during compile phase. Is it possible to generate dynamic view structure like this in SwiftUI?
ViewHierarchy.swift:
import SwiftUI
#main
struct ViewHierarchyApp: App {
var body: some Scene {
WindowGroup {
ContentView(tree: mockData())
}
}
}
func mockData() -> [Tree] {
let tree2: [Tree] = [Tree(id: 2, title: "2", items: [])]
let tree1: [Tree] = [Tree(id: 1, title: "1", items: []),
Tree(id: 11, title: "11", items: []),
Tree(id: 12, title: "12", items: tree2)]
let tree: [Tree] = [Tree(id: 0, title: "Root", items: tree1)]
return tree
}
Model.swift:
class Tree: Identifiable {
let id: Int
let title: String
let items: [Tree]
init(id: Int, title: String, items: [Tree]) {
self.id = id
self.title = title
self.items = items
}
}
ContentView.swift:
struct ContentView: View {
let tree: [Tree]
var body: some View {
VStack {
ForEach(tree, id: \.id) { treeItem in
TreeViewItem(item: treeItem) {
VStack {
Text("-")
}
}
}
}
}
}
struct TreeViewItem<Content: View>: View {
let item: Tree
let content: Content
init(item: Tree, #ViewBuilder content: () -> Content) {
self.item = item
self.content = content()
}
var body: some View {
VStack {
Text(item.title)
ForEach(item.items, id: \.id) { treeItem in
TreeViewItem(item: treeItem) {
VStack {
Text("-")
} as! Content
}
}
}
content
}
}
Output:

SwiftUI NavigationLink in the list doesn't get the right detail page with isActive

I just want to simply navigate to a detail page from a List if press any cell. I have a list like this:
When I click cell c it gets d or others. Rather than this page.
Here is my code:
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var isCellSelected = false
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
isActive: $isCellSelected,
label: {
RowView(text: items[index])
})
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RowView: View {
var text: String
var body: some View {
VStack {
Text(text)
}
}
}
If I remove isActive: $isCellSelected then it works as expected. I need to use isCellSelected to pop back to root view. Not sure how to fix this issue.
Any help? thanks!
EDIT
Update removed isActive and try set selection = nil
truct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: DetailView(text: "\(items[index])", isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
RowView(text: items[index])
})
}
}
}
}
}
When press Back button, doesn't go back.
It is not recommended to share a single isActive state among multiple NavigationLinks.
Why don't you use selection instead of isActive?
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil //<-
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
tag: index, //<-
selection: $selectedTag, //<-
label: {
RowView(text: items[index])
})
}
}
}
}
}
You can set nil to selectedTag to pop back. Seems NavigationLink in List does not work as I expect. Searching for workarounds and update if found.
A dirty workaround:
(Tested with Xcode 12.3/iPhone simulator 14.3. Please do not expect this to work on other versions of iOS including future versions.)
struct DetailView: View {
var text: String
#Binding var isCellSelected: Bool
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = false
}
}
}
}
struct Item {
var text: String
var isActive: Bool = false
}
struct ContentView: View {
#State var items = ["a", "b", "c", "d"].map {Item(text: $0)}
#State var listId: Bool = false //<-
var body: some View {
// Text(items.description) // for debugging
NavigationView {
List {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index].text)",
isCellSelected: $items[index].isActive)
.onAppear{ listId.toggle() } //<-
,
isActive: $items[index].isActive,
label: {
RowView(text: items[index].text)
})
}
}
.id(listId) //<-
}
}
}
Another workaround:
struct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
ZStack {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index])",
isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
EmptyView()
})
}
List {
ForEach(items.indices) { index in
Button(action: {
selectedTag = index
}) {
HStack {
RowView(text: items[index])
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color.secondary)
}
}
}
}
}
}
}
}

Why my SwiftUI List is not getting updated?

I use a SwiftUI List and pass a String to a different view via a Binding.
But the list get not updated when I went back.
Here is my example:
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: String?
#State private var showSheet = false
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = list[i]
showSheet.toggle()
}) {
Text(list[i])
}
}
}
.sheet(isPresented: $showSheet, content: {
DetailView(input: $item)
})
}
}
And the detail page:
struct DetailView: View {
#Binding var input: String?
var body: some View {
Text(input ?? "")
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
What I want to achieve is, that on every Item I click, I see the detail page. After that the element should change to "changed". But this does not happen. Why?
You update item but not list, so don't see any result. Here is corrected variant - store selected index and pass binding to list by index.
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: Int?
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = i
}) {
Text(list[i])
}
}
}
.sheet(item: $item, content: { i in
DetailView(input: $list[i])
})
}
}
extension Int: Identifiable {
public var id: Self { self }
}
struct DetailView: View {
#Binding var input: String
var body: some View {
Text(input)
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
I recommend you use .sheet(item:content:) instead of .sheet(isPresented:content:)
struct ContentView: View {
#State private var items = ["a", "b", "c"]
#State private var selectedIndex: Int?
var body: some View {
List {
ForEach(items.indices) { index in
Button(action: {
selectedIndex = index
}) {
Text(items[index])
}
}
}
.sheet(item: $selectedIndex) { index in
DetailView(item: $items[index])
}
}
}
struct DetailView: View {
#Binding var item: String
var body: some View {
Text(item)
.onDisappear {
print("changed to changed")
item = "changed"
}
}
}
This will, however, require the selectedIndex to conform to Identifiable.
You can either create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}
or create a custom struct for your data (and conform to Identifiable).

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?

Resources