I am developing an App for increasing productivity. My Main Goal in this file is to add Views over a dialog. Another target is to save the data in an Array for using it again with the annotation #AppStorage.
struct Task : Identifiable {
var id = UUID()
var myContent = "Empty"
var myCounter = 0
}
I'm using this struct to save my data which is here mainly the tasks name.
struct TaskView : View {
var task : Task
var body: some View {
HStack {
Spacer()
Text(String(task.myContent) ?? "test")
Spacer()
Text("Sessions today: " + String(task.myCounter))
Spacer()
Image(systemName: "xmark.bin.fill")
}
}
}
For displaying the data I'm using my own struct.
struct ItemList: View {
#AppStorage("myviews") var myviews : [Task]? = nil
#State private var showingAlert = false;
#State private var taskName = "tim";
var body: some View {
VStack{
if(!myviews.isEmpty){
for task in myviews {
TaskView(task: task)
}
}
Spacer()
Button {
showingAlert = true;
} label: {
Image(systemName: "plus")
.padding()
.background(Color.red)
.accentColor(.white)
.cornerRadius(100)
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"), message: TextField("Task: "; text: $taskName), primaryButton: .destructive(Text("Got it!")){
myviews.append(Task(myContent: String(taskName), myCounter: 0))
})
}
Spacer()
}
}
}
So the main part consists of my #AppStorage Array, a loop to show existing "tasks" and a Dialog to add these tasks to the array.
The Error I am getting is the "No exact matches in call to initializer" directly in the line #AppStorage("myviews") var myviews : [Task]? = nil
I already tried different variations of initializing the array until I read in a forum that leaving the initialization not optional could be a cause to my problems.
Furthermore I checked my "Text" - Fields for the wrong types and casted the Int's (myCounter) to String.
It feels like I read every StackOverflow Article regarding my Error but none could help me.
Related
I am very new to programming in Swift. So I'm trying to come up with a time management program. I have posted some code that have been derived from my project that is a work in progress, and I'm trying to troubleshoot some issues that I'm having that come from my lack of knowledge regarding Swift and SwiftUI. I would like to ask two questions here, but if you only have the answer to just one of them, I would greatly appreciate it.
So in my ContentView, I'm trying to display the taskName of the object with ID 0 using a Text in a VStack -- however, it is not displaying, and I'm not sure of the reason why. I can display the taskLength by putting it inside the String method, but taskName is not coming up when I attempt to display it.
Also I'm attempting to change the taskName of Task(id: 0) that is being passed into display2 directly from the display2, but I'm not sure if the taskName of Task(id: 0) is actually being changed, or it's only the taskName of #State var task:Task in display2 that is being changed -- based on my intuitions, I would think the latter case is actually happening. In that case, is there a way to directly edit the taskName of Task(id: 0) from display2?
import SwiftUI
import Foundation
import Combine
struct Task: Hashable, Codable, Identifiable {
var id: Int
var taskName: String = ""
var taskLength: Int = 0
var isBreak : Bool = false
}
class ModelData : ObservableObject{
#Published var tasks: [Task] = [
Task(id: 0,taskName: "Test", taskLength: 34, isBreak: false),
Task(id: 1,taskName: "Math", taskLength: 30, isBreak: false),
Task(id: 2,taskName: "Science", taskLength: 40, isBreak: false)
]
}
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(Task(id: 0).taskName)
display2(task:Task(id: 0))
}
}
}
struct display2: View{
#State var task:Task
var body: some View {
TextField("New task",text: $task.taskName)
}
}
The problem is here:
Text(Task(id: 0).taskName)
Here, you're creating a new Task, with an id of 0. This is not the first task inside your ModelData's tasks array.
Instead, reference the first task via subscript []:
Text(modelData.tasks[ /* index of task */ ].taskName)
Normally you can just put 0 here to get the first Task. However, you said you actually want the Task with an id of 0. You can do this via firstIndex(where:).
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(
modelData.tasks[getTaskIndexFrom(id: 0)] /// access
.taskName
)
Display2( /// see https://stackoverflow.com/a/67064699/14351818
task: $modelData.tasks[getTaskIndexFrom(id: 0)]
)
}
}
func getTaskIndexFrom(id: Int) -> Int {
/// get first index of a task with the specified `id`
if let firstIndex = modelData.tasks.firstIndex(where: { $0.id == 0 }) {
return firstIndex
} else {
return 0
}
}
}
struct Display2: View{
#Binding var task: Task /// need a Binding here
var body: some View {
TextField("New task", text: $task.taskName)
}
}
Ok, your second question:
In that case, is there a way to directly edit the taskName of Task(id: 0) from display2?
Yep! Just use #Binding on Display2's task. This way, all changes will be synced back to your modelData.
In ContentView you used just Task(), but you have to use modelData for #Published var tasks in ModelData.
Task(id: 0).taskName -> modelData.tasks[1].taskName
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(modelData.tasks[1].taskName)
.foregroundColor(.blue)
display2(task:Task(id: 0))
}
}
}
Also, as long as you use #EnvironmentObject, you need to add .environmentObject to the main as well.
(The code below is an example of the SwiftUI life cycle)
import SwiftUI
#main
struct ReplyToStackoverflowApp: App {
var modelData: ModelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
Environment
Xcode 11.2.1 (11B500)
Problem
In order to implement editable teble with TextField on SwiftUI, I used ForEach(0..<items.count) to handle index.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i])
}
}
}
}
However, problems arise if the table is changed to be deleteable.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i]) // Thread 1: Fatal error: Index out of range
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐶" from animals
}
}
}
}
Thread 1: Fatal error: Index out of range when delete item
🐶 has been removed from animals and the ForEach loop seems to be running twice, even though animals.count is 1.
(lldb) po animals.count
1
(lldb) po animals
▿ 1 element
- 0 : "🐱"
Please give me advice on the combination of Foreach and TextField.
Thanks.
Ok, the reason is in documentation for used ForEach constructor (as you see range is constant, so ForEach grabs initial range and holds it):
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
So the solution would be to provide dynamic container. Below you can find a demo of possible approach.
Full module code
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
VStack {
HStack {
EditButton()
Button(action: { self.animals.append("Animal \(self.animals.count + 1)") }, label: {Text("Add")})
}
List {
ForEach(animals, id: \.self) { item in
EditorView(container: self.$animals, index: self.animals.firstIndex(of: item)!, text: item)
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐶" from animals
}
}
}
}
}
struct EditorView : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
it is because editbutton is IN your list. place it ouside or better in navigationbar.
on my project in SwiftUi I'm listing some data in a form, I want to give the user the ability to select a single item in the list with the tap action( can not select more than one in the list)
on the code below I have create a list:
List {
ForEach(listOfCycle, id: \.self) {db in
dbList(db: db, ciclo: self.$cycleSelected)
}
}
and for each row I have the view dbList
import SwiftUI
struct dbList: View {
#State var db : Cycle
#Binding var ciclo : Cycle?
#State var cicloSelected : Bool = false
var body: some View {
HStack{
Text("Database:")
Spacer()
Text(db.idDatabaseAirports ?? "").foregroundColor(self.cicloSelected ? .green: .black).font(self.cicloSelected ? .title : .body)
if self.cicloSelected {
Image(systemName: "checkmark.circle")
}
}.onTapGesture {
self.cicloSelected.toggle()
self.ciclo = self.db
}
}
}
the logic work but the user can tap on multiple row and select more than one, on my project I have to put the checkmark only on one row at the time can't be more than one.
is there any way I can put to avoid multiple selection.
Thanks a lot
The provided code is not testable, so only idea (scratchy, but should be clear).
Note: make sure that Cycle is Equatable
struct dbList: View {
#State var db : Cycle
#Binding var ciclo : Cycle?
var body: some View {
HStack{
Text("Database:")
Spacer()
Text(db.idDatabaseAirports ?? "").foregroundColor(self.db == self.cyclo ? .green: .black).font(self.cicloSelected ? .title : .body)
if self.db == self.cyclo {
Image(systemName: "checkmark.circle")
}
}.onTapGesture {
self.ciclo = self.db
}
}
}
i try to combine two objects from the following types
#ObservedObject var expenses = Expense()
#ObservedObject var recipes = Recipe()
The arrays worked quite good and everything is fine.
Now i would like to present all items from the arrays in a ForEach.
var body: some View {
TabView {
NavigationView {
List {
ForEach(Array(zip(expenses.items, recipes.ReItems)),id: \.0){ item in
HStack{
VStack(alignment: .leading){
Text(item.beschreibung)
.font(.headline)
Text(String(item.menge) + " \(item.unitType)")
}
}
}
.onDelete(perform: removeItems)
}
But this throws an error - "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
My first idea was to save the arrays in a variable like in this stackoverflow post
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
#State private var arrayExpense = self.expenses.items
#State private var arrayRecipes = self.recipes.ReItems
But to be hoonest, this looks not good.. It also throws an exception ;o
Thanks for your help!
Try to break it apart, like below (this gives compilers explicitly type check result of zip)
var body: some View {
TabView {
NavigationView {
List {
self.listContent(items: Array(zip(expenses.items, recipes.ReItems)))
}
...
private func listContent(items: [Item]) -> some View {
ForEach(items, id: \.0){ item in
HStack{
VStack(alignment: .leading){
Text(item.beschreibung)
.font(.headline)
Text(String(item.menge) + " \(item.unitType)")
}
}
}
.onDelete(perform: removeItems)
}
should the Text be:
Text(item.0.beschreibung).font(.headline)
Text(String(item.0.menge) + " \(item.0.unitType)")
or with item.1 as the case maybe.
I'm try to speed up my APP that for now unfortunately is very slow on performing some search and list of data with Swift Ui.
first of all I have a data model that describe my object airport, called AirportModel
import Foundation
class AirportModel : Identifiable , Codable{
var aptICAO : String
var aptName : String
var aptCity : String
var aptCountry :String
var aptIATA : String
init(aptICAO: String, aptName: String, aptCity: String, aptCountry: String, aptIATA: String) {
self.aptICAO = aptICAO
self.aptName = aptName
self.aptCity = aptCity
self.aptCountry = aptCountry
self.aptIATA = aptIATA
}
}
I have a local file apt.json, that contain all the data information for my airport (I downloaded from internet this json, inside there are around 29000 airport)
so when I run the first time the app, with the following function , i create and save the airportVector of type AirportModel.
func openfilejson (fileName : String) {
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
do {
let fileUrl = URL(fileURLWithPath: path)
let datafile = try Data(contentsOf: fileUrl,options: .mappedIfSafe)
let json = JSON(data:datafile)
objectWillChange.send()
for (key,_) in json {
let airport = AirportModel(aptICAO: "", aptName: "", aptCity: "", aptCountry: "", aptIATA: "")
airport.aptName = json[key]["name"].stringValue
airport.aptCity = json[key]["city"].stringValue
airport.aptCountry = json[key]["country"].stringValue
airport.aptIATA = json[key]["iata"].stringValue
airport.aptICAO = json[key].stringValue
airportVector.append(airport)
}
debugPrint("nel vettore airport Vector ci sono \(airportVector.count) aeroporti")
save2() // to save airport vector in the plistfile
debugPrint("SALVATO IN MEMORIA ")
} catch {
print("ERRORE OPEN FILE AEROPORTI")
}
}
}
all, this work fine , the vector with 29000 airport inside is created once the app is ru the first time.
NOW THE BIG ISSUE.
in one view, I try to list this airport using SwiftUI and a searchBar to search on it.
the problem is, due to the big amount of data to be opened, when I load the View with the list; the data take very long to be listed and moreover when I lunch the search everything is stuck!
any idea how can I process or speed up this huge amount of data to avoid the app stuck on the loading of the following view!
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var searchTerm : String = ""
var body: some View {
VStack {
Text("List")
SearchBar(text: $searchTerm).shadow(radius: 10)
List(dm.airportVector.filter{
$0.aptCity.localizedCaseInsensitiveContains(searchTerm)
|| $0.aptName.localizedCaseInsensitiveContains(searchTerm)
}){ item in
HStack {
Text(item.aptICAO).bold().font(Font.system(size:20)).frame(width: 90, height: 10)
//
Text(item.aptName).font(Font.system(size: 15))
Spacer()
VStack(alignment: .leading) {
Text(item.aptCity).font(Font.system(size: 10)).font(.subheadline)
Spacer()
Text(item.aptCountry).font(Font.system(size: 10))
}
}
}
}
}
}
Thanks in advance.
Damiano
Make your data conform to Identifiable and use ForEach inside of your List with a separate View to render the item:
List {
ForEach(items) {
MyItem(item)
}
}
Only render items when they change by making MyItem conform to Equatable, and define the == function. Make sure the == function is being executed by adding a breakpoint or print statement. If it's not being executed see this article for a workaround:
https://swiftui-lab.com/equatableview/
If the above suggestion works, but you find yourself making wholesale modifications to the list after initial render, try using id() during these modifications. See https://swiftui-lab.com/swiftui-id/
If the above suggestions don't improve things enough, you probably need a more custom solution. Use a TableView or CollectionView, see https://github.com/apptekstudios/ASCollectionView