I'm using a ForEach to display the contents of an array, then manually showing a divider between each element by checking the element index. Here's my code:
struct ContentView: View {
let animals = ["Apple", "Bear", "Cat", "Dog", "Elephant"]
var body: some View {
VStack {
/// array of tuples containing each element's index and the element itself
let enumerated = Array(zip(animals.indices, animals))
ForEach(enumerated, id: \.1) { index, animal in
Text(animal)
/// add a divider if the element isn't the last
if index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
}
Result:
This works, but I'd like a way to automatically add dividers everywhere without writing the Array(zip(animals.indices, animals)) every time. Here's what I have so far:
struct ForEachDividerView<Data, Content>: View where Data: RandomAccessCollection, Data.Element: Hashable, Content: View {
var data: Data
var content: (Data.Element) -> Content
var body: some View {
let enumerated = Array(zip(data.indices, data))
ForEach(enumerated, id: \.1) { index, data in
/// generate the view
content(data)
/// add a divider if the element isn't the last
if let index = index as? Int, index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
/// usage
ForEachDividerView(data: animals) { animal in
Text(animal)
}
This works great, isolating all the boilerplate zip code and still getting the same result. However, this is only because animals is an array of Strings, which conform to Hashable — if the elements in my array didn't conform to Hashable, it wouldn't work:
struct Person {
var name: String
}
struct ContentView: View {
let people: [Person] = [
.init(name: "Anna"),
.init(name: "Bob"),
.init(name: "Chris")
]
var body: some View {
VStack {
/// Error! Generic struct 'ForEachDividerView' requires that 'Person' conform to 'Hashable'
ForEachDividerView(data: people) { person in
Text(person.name)
}
}
}
}
That's why SwiftUI's ForEach comes with an additional initializer, init(_:id:content:), that takes in a custom key path for extracting the ID. I'd like to take advantage of this initializer in my ForEachDividerView, but I can't figure it out. Here's what I tried:
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
var data: Data
var id: KeyPath<Data.Element, ID>
var content: (Data.Element) -> Content
var body: some View {
let enumerated = Array(zip(data.indices, data))
/// Error! Invalid component of Swift key path
ForEach(enumerated, id: \.1.appending(path: id)) { index, data in
content(data)
if let index = index as? Int, index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
/// at least this part works...
ForEachDividerView(data: people, id: \.name) { person in
Text(person.name)
}
I tried using appending(path:) to combine the first key path (which extracts the element from enumerated) with the second key path (which gets the Hashable property from the element), but I got Invalid component of Swift key path.
How can I automatically add a divider in between the elements of a ForEach, even when the element doesn't conform to Hashable?
Simple way
struct ContentView: View {
let animals = ["Apple", "Bear", "Cat", "Dog", "Elephant"]
var body: some View {
VStack {
ForEach(animals, id: \.self) { animal in
Text(animal)
if animals.last != animal {
Divider()
.background(.blue)
}
}
}
}
}
Typically the type inside animals must be Identifiable. In which case the code will be modified as.
if animals.last.id != animal.id {...}
This will avoid any equatable requirements/ implementations
Does everything need to be in a ForEach? If not, you can consider not using indices at all:
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
var data: Data
var id: KeyPath<Data.Element, ID>
var content: (Data.Element) -> Content
var body: some View {
if let first = data.first {
content(first)
ForEach(data.dropFirst(), id: id) { element in
Divider()
.background(.blue)
content(element)
}
}
}
}
Found a solution!
appending(path:) seems to only work on key paths erased to AnyKeyPath.
Then, appending(path:) returns an optional AnyKeyPath? — this needs to get cast down to KeyPath<(Data.Index, Data.Element), ID> to satisfy the id parameter.
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
var data: Data
var id: KeyPath<Data.Element, ID>
var content: (Data.Element) -> Content
var body: some View {
let enumerated = Array(zip(data.indices, data))
/// first create a `AnyKeyPath` that extracts the element from `enumerated`
let elementKeyPath: AnyKeyPath = \(Data.Index, Data.Element).1
/// then, append the `id` key path to `elementKeyPath` to extract the `Hashable` property
if let fullKeyPath = elementKeyPath.appending(path: id) as? KeyPath<(Data.Index, Data.Element), ID> {
ForEach(enumerated, id: fullKeyPath) { index, data in
content(data)
if let index = index as? Int, index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
}
Usage:
struct Person {
var name: String
}
struct ContentView: View {
let people: [Person] = [
.init(name: "Anna"),
.init(name: "Bob"),
.init(name: "Chris")
]
var body: some View {
VStack {
ForEachDividerView(data: people, id: \.name) { person in
Text(person.name)
}
}
}
}
Result:
Related
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:
I'm trying to make an app that is displaying lists with selections/checkmarks based on clicked NavigationLink. The problem I encountered is that my selections disappear when I go back to main view and then I go again inside the NavigationLink. I'm trying to save toggles value in UserDefaults but it's not working as expected. Below I'm pasting detailed and main content view.
Second view:
struct CheckView: View {
#State var isChecked:Bool = false
#EnvironmentObject var numofitems: NumOfItems
var title:String
var count: Int=0
var body: some View {
HStack{
ScrollView {
Toggle("\(title)", isOn: $isChecked)
.toggleStyle(CheckToggleStyle())
.tint(.mint)
.onChange(of: isChecked) { value in
if isChecked {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
UserDefaults.standard.set(self.isChecked, forKey: "locationToggle")
}.onTapGesture {
}
.onAppear {
self.isChecked = UserDefaults.standard.bool(forKey: "locationToggle")
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
Main view:
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach(menu){
section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach(section.items) { item in
CheckView( title: item.name)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
ItemsSection:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria"
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola"
}
Model:
struct ItemsSection: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var items: [CountriesItem]
}
struct CountriesItem: Codable, Equatable, Identifiable,Hashable {
var id: UUID = UUID()
var name: String
}
As allready stated in the comment you have to relate the isChecked property to the CountryItem itself. To get this to work i have changed the model and added an isChecked property. You would need to add this to the JSON by hand if the JSON allread exists.
struct CheckView: View {
#EnvironmentObject var numofitems: NumOfItems
//use a binding here as we are going to manipulate the data coming from the parent
//and pass the complete item not only the name
#Binding var item: CountriesItem
var body: some View {
HStack{
ScrollView {
//use the name and the binding to the item itself
Toggle("\(item.name)", isOn: $item.isChecked)
.toggleStyle(.button)
.tint(.mint)
// you now need the observe the isChecked inside of the item
.onChange(of: item.isChecked) { value in
if value {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
}.onTapGesture {
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach($menu){ // from here on you have to pass a binding on to the decendent views
// mark the $ sign in front of the property name
$section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach($section.items) { $item in
//Pass the complete item to the CheckView not only the name
CheckView(item: $item)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
Example JSON:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria",
"isChecked": false
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola",
"isChecked": false
}
]
}
]
Remarks:
The aproach with JSON and storing this in the bundle will prevent you from persisting the isChecked property between App launches. Because you cannot write to the Bundle from within your App. The choice will persist as long as the App is active but will be back to default as soon as you either reinstall or force quit it.
As already mentioned in the comment, I don'r see where you read back from UserDefaults, so whatever gets stored there, you don't read it. But even if so, each Toggle is using the same key, so you are overwriting the value.
Instead of using the #State var isChecked, which is used just locally, I'd create another struct item which gets the title from the json and which contains a boolean that gets initialized with false.
From what I understood, I assume a solution could look like the following code. Just a few things:
I am not sure how your json looks like, so I am not loading from a json, I add ItemSections Objects with a title and a random number of items (actually just titles again) with a function.
Instead of a print with the number of checked toggles, I added a text output on the UI. It shows you on first page the number of all checked toggles.
Instead of using UserDefaults I used #AppStorage.
To make that work you have to make Array conform to RawRepresentable you achieve that with the following code/extension (just add it once somewhere in your project)
Maybe you should thing about a ViewModel (e.g. ItemSectionViewModel), to load the data from the json and provide it to the views as an #ObservableObject.
The code for the views:
//
// CheckItem.swift
// CheckItem
//
// Created by Sebastian on 24.08.22.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack() {
CheckItemView()
}
}
}
struct CheckItemView: View {
let testStringForTestData: String = "Check Item Title"
#AppStorage("itemSections") var itemSections: [ItemSection] = []
func addCheckItem(title: String, numberOfItems: Int) {
var itemArray: [Item] = []
for i in 0...numberOfItems {
itemArray.append(Item(title: "item \(i)"))
}
itemSections.append(ItemSection(title: title, items: itemArray))
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in itemSections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i + filteredItems.count
}
return i
}
var body: some View {
NavigationView{
VStack() {
List(){
ForEach(itemSections.indices, id: \.self){ id in
NavigationLink(destination: ItemSectionDetailedView(items: $itemSections[id].items)) {
Text(itemSections[id].title)
}
.padding()
}
}
Text("Number of checked items: \(self.getSelectedItemsCount())")
.padding()
Button(action: {
self.addCheckItem(title: testStringForTestData, numberOfItems: Int.random(in: 0..<4))
}) {
Text("Add Item")
}
.padding()
}
}
}
}
struct ItemSectionDetailedView: View {
#Binding var items: [Item]
var body: some View {
ScrollView() {
ForEach(items.indices, id: \.self){ id in
Toggle(items[id].title, isOn: $items[id].isOn)
.padding()
}
}
}
}
struct ItemSection: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var items: [Item]
}
struct Item: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var isOn: Bool = false
}
Here the adjustment to work with #AppStorage:
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
i am a newbie in IOS development, so I wanted to do something like when the user scroll and reaches the bottom of the list, the list updates and append it data. basically an infinite scrolling.
But I have no idea other than setting it via engineering the offset, it would take a lot of time and effort + might not be that clean/brilliant solution. Are there any other solution other than using a 3rd person library?
Thank you
here's my code :
ForEach(homeContent.MainContent){data in
homeContentItem(name: data.name ?? "", image: data.image ?? "", released: data.released ?? "")
.padding(.horizontal)
.onAppear{
homeContent.appendData(currentItem: data)
}
}
What this code does is well infinite scrolling, but it'll always update and that is something that I don't want to, I want it when user scroll to the bottom first in order to update the list
Note : if there any similar question, please do comment it so that I could delete this one I guess
You were close with onAppear, you only missing checking index of item. You need to append new data only when last element is visible, not for anyone. I suggest you using ForEachIndexed:
struct ContentView: View {
#State
var items = (0...100).map { _ in Item() }
var body: some View {
ScrollView {
LazyVStack {
ForEachIndexed(items) { i, item in
Text("\(i) \(item.id)")
.onAppear {
if i == items.indices.last {
items += (0...10).map { _ in Item() }
print("new items added", i, items.count)
}
}
}
}
}
}
}
struct ForEachIndexed<Data: RandomAccessCollection, Content: View, ID: Hashable>: View {
let data: [EnumeratedSequence<Data>.Element]
let content: (Int, Data.Element) -> Content
let id: KeyPath<EnumeratedSequence<Data>.Element, ID>
init(_ data: Data, #ViewBuilder content: #escaping (Int, Data.Element) -> Content) where Data.Element: Identifiable, ID == Data.Element.ID {
self.data = Array(data.enumerated())
self.content = content
self.id = \.element.id
}
var body: some View {
ForEach(data, id: id) { element in
content(element.offset, element.element)
}
}
}
struct Item: Identifiable {
let id: String
init() {
id = UUID().uuidString
}
}
I have read several solutions to this issue which I believe I am doing correctly based on suggestions but this simple code still crashes.
import SwiftUI
struct AnItem : Identifiable {
let id = UUID()
var text: String
}
struct ContentView: View {
#State private var items : [AnItem] = [AnItem(text: "A"), AnItem(text: "B"), AnItem(text: "C")]
var body: some View {
List () {
ForEach (0 ..< items.count) {
index in
TextField("test", text: $items[index].text)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.items.remove(atOffsets: indexSet)
}
}
Anybody have an idea for a workaround, or maybe I'm just doing this wrong. The crash says
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
As others have stated, just a Text widget works fine. This appears to be induced by using TextField.
Here is a possible solution. Using id:\.id to identity the items and then extract the TextField to another view
struct AnItem : Identifiable {
let id = UUID()
var text: String
}
struct ContentView: View {
#State private var items : [AnItem] = [AnItem(text: "A"), AnItem(text: "B"), AnItem(text: "C")]
var body: some View {
List {
ForEach(items, id: \.id) { item in
CustomTextField(item: item)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
items.remove(atOffsets: indexSet)
}
}
struct CustomTextField : View {
#State var item : AnItem
var body : some View {
TextField("Test", text: $item.text)
}
}
How about doing it like this:
First lets make AnItem conform to Hashable protocol.
struct AnItem : Identifiable, Hashable {
let id = UUID()
var text: String
}
Now let's change your Foreach and List a little bit:
List {
ForEach (items, id: \.self) { item in
Text("\(item.text)")
}.onDelete(perform: deleteItem)
}
Voila! everything is working perfectly now!
I have a Picker embedded in Form, however I can't get it working that it shows a checkmark and the selected value in the form.
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
ForEach(0 ..< codes.count) {
Text(self.codes[$0]).tag($0)
}
}
}
}
}
TL;DR
Your variable currencyCode does not match the type of the ID for each element in your ForEach. Either iterate over the codes in your ForEach, or pass your Picker an index.
Below are three equivalent examples. Notice that the #State variable which is passed to Picker always matches the ID of element that the ForEach iterates over:
Also note that I have picked a default value for the #State variable which is not in the array ("", -1, UUID()), so nothing is shown when the form loads. If you want a default option, just make that the default value for your #State variable.
Example 1: Iterate over codes (i.e. String)
struct ContentView: View {
#State private var currencyCode: String = ""
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
// ID is a String ----v
ForEach(codes, id: \.self) { (string: String) in
Text(string)
}
}
}
}
}
}
}
Example 2: Iterate over indices (i.e. Int)
struct ContentView: View {
#State private var selectedIndex: Int = -1
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedIndex, label: Text("Currency")) {
// ID is an Int --------------v
ForEach(codes.indices, id: \.self) { (index: Int) in
Text(self.codes[index])
}
}
}
}
}
}
}
Example 3: Iterate over an identifiable struct by its ID type (i.e. UUID)
struct Code: Identifiable {
var id = UUID()
var value: String
init(_ value: String) {
self.value = value
}
}
struct ContentView: View {
#State private var selectedUUID = UUID()
var codes = [Code("EUR"), Code("GBP"), Code("USD")]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedUUID, label: Text("Currency")) {
// ID is a UUID, because Code conforms to Identifiable
ForEach(self.codes) { (code: Code) in
Text(code.value)
}
}
}
}
}
}
}
It's difficult to say, what you are doing wrong, because your example doesn't include the declaration of codes or currencyCode. I suspect that the problem is with the binding being of a different type than the tag you are setting on a picker (which is an Int in your case).
Anyway, this works:
struct ContentView: View {
let codes = Array(CurrencyCode.allCases)
#State private var currencyCode: CurrencyCode?
var body: some View {
NavigationView {
Form {
Section {
Picker("Currency",
selection: $currencyCode) {
ForEach(codes, id: \.rawValue) {
Text($0.rawValue).tag(Optional<CurrencyCode>.some($0))
}
}
}
}
}
}
}
enum CurrencyCode: String, CaseIterable {
case eur = "EUR"
case gbp = "GBP"
case usd = "USD"
}