I have a problem programmatically opening and closing a View in SwiftUI:
With the code below SwiftUI opens each index of contactsArray one after another, when clicking on one of them (it loops through all of them). Of course it should just open the one I clicked on.
I thought the problem might rely on the id but my Model is Identifiable.
ContactsView:
// ...
List {
ForEach(contactsViewModel.contactsArray, id: \.self) {
contact in
NavigationLink(destination: ContactsDetailsView(contact: contact), isActive: self.$userViewModel.showContacts) {
Text(contact.surname).bold() + Text(", ") + Text(contact.forename)
}
}
}
ContactsViewModel:
final class ContactsViewModel: ObservableObject {
#Published var contactsArray: [ContactModel] = []
// ...
}
ContactModel:
struct ContactModel: Decodable, Identifiable, Equatable, Hashable, Comparable {
var id: String
var surname: String
var forename: String
var telephone: String
var email: String
var picture: String
var gender: String
var department: String
static func < (lhs: ContactModel, rhs: ContactModel) -> Bool {
if lhs.surname != rhs.surname {
return lhs.surname < rhs.surname
} else {
return lhs.forename < rhs.forename
}
}
static func == (lhs: ContactModel, rhs: ContactModel) -> Bool {
return lhs.surname == rhs.surname && lhs.forename == rhs.forename
}
}
You join all NavigationLink/s to one state (one-to-many), so, no surprise, when you toggle this state all links are activated.
Instead you need something like the following
#State private var selectedContact: String? = nil // or put it elsewhere
...
NavigationLink(destination: ContactsDetailsView(contact: contact),
tag: contact.id, selection: $selectedContact) {
Text(contact.surname).bold() + Text(", ") + Text(contact.forename)
}
, where selectedContact is id of contact link to be activated. Then all you need is to decide which contact id to assign to selectedContact.
Related
I'm having a bit of a complicated construction and have trouble figuring out how to get it working:
class Parent : Codable, ObservableObject {
#Published public var children: [Child]?
public func getChildren(with name: String) -> [Child] {
return children?.filter { $0.name == name } ?? []
}
}
class Child : Codable, Hashable, ObservableObject {
static func == (lhs: Child, rhs: Child) -> Bool {
return lhs.name == rhs.name && lhs.isSomething == rhs.isSomething
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(isSomething)
}
let name: String
#Published var isSomething: Bool
}
...
struct MyView : View {
#ObservedObject var parent: Parent
var names: [String]
var body: some View {
ForEach(names, id: \.self) { name in
...
ForEach(parent.getChildren(with: name), id: \.self) { child in
Toggle(isOn: child.$isSomething) { <== ERROR HERE
...
}
}
}
}
}
I had also tried Toggle(isOn: $child.isSomething) which of course leads to Cannot find '$child' in scope.
How do I resolve this? In more detail: How do I return the correct type from getChildren() that allows $child.isSomething for example?
(BTW, I used this to allow an ObservableObject class to be Codable. Although this seems unrelated, I've let this into my code extraction above because perhaps it matters.)
We can use separate #Published property with .onChange .
struct ContentView: View {
#ObservedObject var parent: Parent
#State var names: [String] = ["test1", "test2"]
var body: some View {
ForEach(names, id: \.self) { name in
GroupBox { // just added for the clarity
ForEach($parent.filteredChildren) { $child in
Toggle(isOn: $child.isSomething) {
Text(child.name)
}
}
}
}
.onChange(of: names) { newValue in
parent.updateChildren(with: "test") //<- here
}
.onAppear{
names.append("test3")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(parent: Parent(children:[ Child(name: "test"), Child(name: "test2")]))
}
}
class Parent: ObservableObject {
init(children: [Child]) {
self.children = children
}
#Published public var children: [Child]
#Published public var filteredChildren: [Child] = []
func updateChildren(with name: String) {
filteredChildren = children.filter { $0.name == name }
}
}
class Child: ObservableObject, Identifiable {
init(name: String) {
self.name = name
}
let id = UUID().uuidString
let name: String
#Published var isSomething: Bool = false
}
I am fairly new to iOS development. I am trying to update the property "cars" in the "Sire" model using a stepper. Whenever I press on + or - from the stepper controls, it changes the value by a step and then becomes disabled.
If I bind the stepper to the variable cars, it works flawlessly.
struct AddSireView: View {
// #EnvironmentObject var sireVM:SiresViewModel
#State var newSire = Sire (id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
#State var cars = 0
#State var cups = 0
#State private var state = FormState.idle
var createAction: CreateAction
// TODO: Put validation that the added sire is valid and if not show errors to the user
var body: some View {
Form {
VStack (spacing: 18) {
TitledTextView(text: $newSire.name, placeHolder: "الاسم", title: "الاسم")
TiltedTextEditor(text: Binding<String>($newSire.info)!, title: "معلومات البعير")
TiltedTextEditor(text: Binding<String>($newSire.achievments)!, title: "انجازات البعير")
}
Stepper(value: $newSire.cars, in: 0...10,step:1) {
HStack {
Text ("سيارات:")
TextField("Cars", value: $newSire.cars, formatter: NumberFormatter.decimal)
}
}
And this is the "Sire" struct
struct Sire: Hashable, Identifiable, Decodable {
static func == (lhs: Sire, rhs: Sire) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.ownerID == rhs.ownerID
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
hasher.combine(ownerID)
}
var id:String
var name:String
var ownerID:Int
var fatherID:String?
var info:String?
var achievments:String?
var cars:Int = 0
var cups:Int = 0
init (id:String, name:String, ownerID:Int, info:String? = nil, achievments:String? = nil,
fatherID:String? = nil, cars:Int = 0, cups:Int = 0) {
self.id = id
self.name = name
self.ownerID = ownerID
self.cars = cars
self.cups = cups
self.info = info
self.achievments = achievments
}
}
"Sire" was a class and i made it a Struct thinking that that was the problem, but to no avail.
Consider this approach using an ObservableObject to hold your Sire. This allows you to use
both the Stepper and the Textfield at the same time.
struct ContentView: View {
#StateObject var sireModel = SireModel() // <-- here
var body: some View {
Form {
Stepper(value: $sireModel.sire.cars, in: 0...10, step:1) {
HStack {
Text ("سيارات: ")
TextField("", value: $sireModel.sire.cars, formatter: NumberFormatter())
}
}
}
}
}
class SireModel: ObservableObject {
#Published var sire: Sire = Sire(id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
}
Get rid of the custom implementations for Equatable and Hashable (func == and func hash) you don't include cars in it so SwiftUI doesn't know when to reload.
SwiftUI is all about identity if you change how Swift computes the identity (using Hashable, Equatable and Identifiable) you change the behavior.
Check out Demystify SwiftUI
The video above is the "best" place to learn about the concept.
I'm just starting out in SwuiftUI so bear with me.
I have a Game stuct that has a field lastUpdated and title. I want to have the choice to sort by my list by lastUpdated or title. But I'm not sure how this works. I've looked into sorted(by:) but I can't really get anything to work. Suggestions?
struct Game: Identifiable, Codable {
let id: UUID
var title: String
var players: [Player]
var lastUsed: Date }
GameView
struct GameListView: View {
#Binding var games: [Game]
var body: some View {
List {
ForEach($games) { $game in
NavigationLink(destination: GameView(game: $game)) {
Text(game.title)}
}
}
The scenario is slightly complicated by the Binding form of ForEach, but you should still be able to return a sorted collection. It might look like this:
struct GameListView: View {
#Binding var games: [Game]
#State var sortType : SortType = .lastUsed
enum SortType {
case lastUsed, title
}
var sortedGames : [Binding<Game>] {
$games.sorted(by: { a, b in
switch sortType {
case .lastUsed:
return a.wrappedValue.lastUsed < b.wrappedValue.lastUsed
case .title:
return a.wrappedValue.title < b.wrappedValue.title
}
})
}
var body: some View {
List {
ForEach(sortedGames) { $game in
NavigationLink(destination: GameView(game: $game)) {
Text(game.title)}
}
}
}
}
I have a ListView that is a few levels deep in a NavigationView. The idea is when a user selects an item in the ListView, the item is stored and the UI pops back to the root NavigationView.
Specific Implementation (Working)
class SpecificListItem: ObservableObject, Codable, Hashable, Identifiable {
var id: String
var kind: String
var name: String
var mimeType: String
init(id: String, kind: String, name: String, mimeType: String) {
self.id = id
self.kind = kind
self.name = name
self.mimeType = mimeType
}
static func == (lhs: DriveListItem, rhs: DriveListItem) -> Bool {
return lhs.id == rhs.id &&
lhs.kind == rhs.kind &&
lhs.name == rhs.name &&
lhs.mimeType == rhs.mimeType
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
func processItem() {
// do backend stuff
}
}
...
struct SpecificListView: View {
#EnvironmentObject var selectedItem: SpecificListItem
// load excluded for brevity
#ObservedObject var item: MyResponse<SpecificListResponse>
var body: some View {
List(selection: $selectedItem, content: {
ForEach(item.data?.files ?? []) {
file in HStack {
Text(file.name)
}
.onTapGesture {
// pop to root view
}
}
})
}
}
This was good for a proof of concept. Now I need to add a layer of abstraction.
Attempt at Generic Implementation
The selected item in this view should be generic. I have the following protocol and class defined that the selected item should conform to:
protocol SelectableItem: ObservableObject, Identifiable, Hashable, Codable {
var id: String { get set }
func process()
}
class GenericListItem: SelectableItem {
var id: String = ""
var name: String = ""
init () { }
init(id: String) {
self.id = id
}
static func == (lhs: GenericListItem, rhs: GenericListItem) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
func process() {
// do backend stuff
}
}
class SpecificListItem: GenericListItem {
var type: String
init () { }
init(id: String) {
self.id = id
}
override func process() {
// do backend stuff
}
}
...
struct GenericListView: View {
#EnvironmentObject var selectedItem: GenericListItem
// load excluded for brevity
#ObservedObject var item: MyResponse<SpecificListItem>
var body: some View {
List(selection: $selectedItem, content: {
ForEach(item.data?.files ?? []) {
file in HStack {
Text(file.name)
}
.onTapGesture {
// pop to root view
}
}
})
}
}
The selected item can have it's own data elements, but I am at least expecting each item to have an id and a process() method that interacts with my backend.
However, when I try to bind the selectedItem in the ListView, I get the following error: Generic parameter 'SelectionValue' could not be inferred.
I am assuming this is because the types do not match. I have read there isn't really any dynamic or run-time type binding possible in Swift? Is this true?
I am a little more experienced in Java, and I know I could achieve this through generics and polymorphism.
SpecificListItem IS-A GenericListItem yet Swift doesn't seem to like the design. (And MyResponse has-a SpecificListItem).
How can I define a generic class that can be selected from many child views and bound at the root view, yet can be inherited to implement their own specific logic?
Edit
My abstraction layer explanation was unclear if not just wrong. So I tried to fix that.
Adding some more information surrounding the code structure and design. Maybe it will help give some more background and context on what I am trying to accomplish.
Here is a small representation of the object interactions:
The idea is I will have many detail views in the application. The backend will return various response types, all conforming to SelectableItem. In this case, the detail view item source is of type SpecificListItem.
And here is the interaction between the user, backend, and UI:
And the idea here being:
User navigates to Detail View
Backend call is made, returns a list of SpecificListItem
List View is populated with items of type SpecifcListItem.
When a user selects an item from Detail View, the application pops back to root, and the selectedItem is set in the root view which is of type GenericListItem.
I am making a personal project to study SwiftUI. All was going well, the I noticed a bug on my app.
I have the simple view bellow, that saves a description, a value and some tags on my ViewModel. I am having an issue with the $viewModel.value. That variable is not being filled with values from the view.
I supose that my #Published var value: Double? from my ViewModel should be updated whenever the user types some value. Thing is, it is not updating on any iPhone 11 and up, but it works perfectly on the iPhone 8.
public struct AddBillView: View {
#ObservedObject private var viewModel: AddBillViewModel
#Environment(\.presentationMode) var presentationMode
public let onExpenseCreated: ((_ expense: Expense)->Void)
public var body: some View {
Text("Add Expense")
VStack {
TextField("Descrição", text: $viewModel.name)
HStack {
Text("Valor \(NumberFormatter.currency.currencySymbol)")
CurrencyTextField("Value", value: $viewModel.value)
.multilineTextAlignment(TextAlignment.leading)
}
HStack {
Text("Tags")
TextField("car pets home",
text: $viewModel.tags)
}
Picker("Type", selection: $viewModel.type) {
Text("Paid").tag("Paid")
Text("Unpaid").tag("Unpaid")
Text("Credit").tag("Credit")
}
}.navigationTitle("+ Expense")
Button("Adicionar") {
if !viewModel.hasExpense() {
return
}
onExpenseCreated(viewModel.expense())
self.presentationMode.wrappedValue.dismiss()
}
}
public init(viewModel outViewModel: AddBillViewModel,
onExpenseCreated: #escaping ((_ expense: Expense)->Void)) {
self.viewModel = outViewModel
self.onExpenseCreated = onExpenseCreated
}
}
And I have a ViewModel:
public class AddBillViewModel: ObservableObject {
#Published var name: String = ""
#Published var type: String = "Paid"
#Published var tags: String = ""
#Published var value: Double?
init(expense: Expense?=nil) {
self.name = expense?.name ?? ""
self.type = expense?.type.rawValue ?? "Paid"
self.tags = expense?.tags?.map { String($0.name) }.joined(separator: " ") ?? ""
self.value = expense?.value
}
func hasExpense() -> Bool {
if self.name.isEmpty ||
self.value == nil ||
self.value?.isZero == true {
return false
}
return true
}
func expense() -> Expense {
let tags = self.tags.split(separator: " ").map { Tag(name: String($0)) }
return Expense(name: self.name, value: self.value ?? 0.0 ,
type: ExpenseType(rawValue: self.type)!,
id: UUID().uuidString,
tags: tags)
}
}
Then I use my view:
AddBillView(viewModel: AddBillViewModel()) { expense in
viewModel.add(expense: expense)
viewModel.state = .idle
}
I already google it and spend a couple of hours looking for an answer, with no luck. Someone have any ideas?
Edited
Here is the code for the CurrencyTextField. I`m using this component:
https://github.com/youjinp/SwiftUIKit/blob/master/Sources/SwiftUIKit/views/CurrencyTextField.swift
But the component works perfectly fine on iPhone 8 simulator and with a #State property inside my view. It does not work only with my ViewModel
I figured it out! The problem was that my AddBillViewModel is an ObservableObject and I was marking each property with #Published. This was causing some kind of double observable object.
I removed the #Published and it started working again.