How does one use NavigationLink isActive binding when working with List in SwiftUI? - ios

The use case is pretty simple. I have a List of places, and each corresponds to a geofence. I need to enable navigation in that particular row(s) whose geofence the user is inside. And the list is dynamic and is fetched from an API. There are similar question on Stackoverflow, but those address only static lists.
I tried using a dictionary of bools, but I am not able to make it work.
This is a simplified mock code:
struct ListView: View {
#State private var navActive: [UUID: Bool] = [:]
var body: some View {
List (viewModel.allItems, id: \.id) { item in
NavigationLink(destination: DetailView(item), isActive: $navActive[item.id]) {
Text(item.name)
.onTapGesture {
if currentLocation.isInside(item.geofence) { navActive[item.id] = true }
}
}
}
}
}
I get this error: Cannot convert value of type 'Binding<Bool?>' to expected argument type 'Binding<Bool>'
on the NavigationLink isActive argument.
Note: I've populated the navActive dictionary with key-value pairs on receiving allItems with an onRecieve modifier

Here is a possible approach for your use-case
struct ListView: View {
// ... your view model defined here
#State private var selectedItem: UUID? = nil
var body: some View {
List (viewModel.allItems, id: \.id) { item in
Text(item.name)
.onTapGesture {
self.selectedItem = item.id
}
.background(
NavigationLink(destination: DetailView(item), tag: item.id,
selection: $selectedItem) { EmptyView() }
.buttonStyle(PlainButtonStyle())
)
}
}
}

Related

Why does this SwiftUI List require an extra objectWillChange.send?

Here is a simple list view of "Topic" struct items. The goal is to present an editor view when a row of the list is tapped. In this code, tapping a row is expected to cause the selected topic to be stored as "tappedTopic" in an #State var and sets a Boolean #State var that causes the EditorV to be presented.
When the code as shown is run and a line is tapped, its topic name prints properly in the Print statement in the Button action, but then the app crashes because self.tappedTopic! finds tappedTopic to be nil in the EditTopicV(...) line.
If the line "tlVM.objectWillChange.send()" is uncommented, the code runs fine. Why is this needed?
And a second puzzle: in the case where the code runs fine, with the objectWillChange.send() uncommented, a print statement in the EditTopicV init() shows that it runs twice. Why?
Any help would be greatly appreciated. I am using Xcode 13.2.1 and my deployment target is set to iOS 15.1.
Topic.swift:
struct Topic: Identifiable {
var name: String = "Default"
var iconName: String = "circle"
var id: String { name }
}
TopicListV.swift:
struct TopicListV: View {
#ObservedObject var tlVM: TopicListVM
#State var tappedTopic: Topic? = nil
#State var doEditTappedTopic = false
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(tlVM.topics) { topic in
Button(action: {
tappedTopic = topic
// why is the following line needed?
tlVM.objectWillChange.send()
doEditTappedTopic = true
print("Tapped topic = \(tappedTopic!.name)")
}) {
Label(topic.name, systemImage: topic.iconName)
.padding(10)
}
}
}
Spacer()
}
.sheet(isPresented: $doEditTappedTopic) {
EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
}
}
}
EditTopicV.swift (Editor View):
struct EditTopicV: View {
#ObservedObject var tlVM: TopicListVM
#Environment(\.presentationMode) var presentationMode
let originalTopic: Topic
#State private var editTopic: Topic
#State private var ic = "circle"
let iconList = ["circle", "leaf", "photo"]
init(tlVM: TopicListVM, originalTopic: Topic) {
print("DBG: EditTopicV: originalTopic = \(originalTopic)")
self.tlVM = tlVM
self.originalTopic = originalTopic
self._editTopic = .init(initialValue: originalTopic)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
editTopic.iconName = editTopic.iconName.lowercased()
tlVM.change(topic: originalTopic, to: editTopic)
presentationMode.wrappedValue.dismiss()
}
}
HStack {
Text("Name:")
TextField("name", text: $editTopic.name)
Spacer()
}
Picker("Color Theme", selection: $editTopic.iconName) {
ForEach(iconList, id: \.self) { icon in
Text(icon).tag(icon)
}
}
.pickerStyle(.segmented)
Spacer()
}
.padding()
}
}
TopicListVM.swift (Observable Object View Model):
class TopicListVM: ObservableObject {
#Published var topics = [Topic]()
func append(topic: Topic) {
topics.append(topic)
}
func change(topic: Topic, to newTopic: Topic) {
if let index = topics.firstIndex(where: { $0.name == topic.name }) {
topics[index] = newTopic
}
}
static func ex1() -> TopicListVM {
let tvm = TopicListVM()
tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
tvm.append(topic: Topic(name: "photos", iconName: "photo"))
tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
return tvm
}
}
Here's what the list looks like:
Using sheet(isPresented:) has the tendency to cause issues like this because SwiftUI calculates the destination view in a sequence that doesn't always seem to make sense. In your case, using objectWillSend on the view model, even though it shouldn't have any effect, seems to delay the calculation of your force-unwrapped variable and avoids the crash.
To solve this, use the sheet(item:) form:
.sheet(item: $tappedTopic) { item in
EditTopicV(tlVM: tlVM, originalTopic: item)
}
Then, your item gets passed in the closure safely and there's no reason for a force unwrap.
You can also capture tappedTopic for a similar result, but you still have to force unwrap it, which is generally something we want to avoid:
.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}

Missing parameter but need parameter to be changed based on user input

This file is for the main structure of the application. This is where the error is coming from which is "Missing argument for parameter 'numberOfDoors' in call". This is because it wants me to add
ContentView(numberOfDoors: <#Int#>)
but im having trouble finding out how I can get what the user chooses to be the int instead of me putting a number in there statically.
import SwiftUI
#main
struct NumOfDoorsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This is my project file.
import SwiftUI
struct ContentView: View {
#State var numberOfDoors: Int
#State var multiOptions: Array<String>
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.multiOptions = [String](repeating: "", count: numberOfDoors)
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
One of the most important parts of SwiftUI programming is creating an appropriate model for your views.
#State properties are OK for local, often independent properties, but they typically aren't a good solution for your main data model or where a model needs to be manipulated by the user.
In your case you want the size of the array to change based on the selected number of doors, so you need somewhere for that procedural code to live; The model is that place.
Here is a simple model object you can use
class DoorModel: ObservableObject {
#Published var numberOfDoors: Int {
didSet {
self.adjustArray(newSize: numberOfDoors)
}
}
#Published var doors:[String]
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.doors = [String](repeating: "", count: numberOfDoors)
}
private func adjustArray(newSize: Int) {
let delta = newSize - doors.count
print("new size = \(newSize) Delta = \(delta)")
if delta > 0 {
doors.append(contentsOf:[String](repeating: "", count: delta))
} else if delta < 0 {
doors.removeLast(-delta)
}
}
}
Note that you still need to supply a starting number of doors via the initialiser for your model. Whenever that value changes, the didSet property observer calls a function to add or remove elements from the end of the array.
You can use this model in your view with an #StateObject decorator. This ensures that a single instance is created and reused as your view is redrawn
struct ContentView: View {
#StateObject var model = DoorModel(numberOfDoors: 1)
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $model.numberOfDoors) {
ForEach(1 ..< 64) { index in
Text("\(index) doors").tag(index)
}
}
ForEach($model.doors.indices, id: \.self) { index in
TextField("Enter your option...", text: $model.doors[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}
}
}
I added the .tag modifier to ensure that the picker works correctly with your 1-based list; By default the tag will be 0 based.
There are, to my opinion, two issues with your code.
Don't initialize ContentView with a parameter. Especially, since you want to start with a value of 0 doors and let the user choose how many doors.
You can't initialize an #State value directly like: self.numberOfDoors = numberOfDoors.
I suggest the following code, it worked for me.
import SwiftUI
struct ContentView: View {
#State private var numberOfDoors: Int
#State private var multiOptions: Array<String>
init() {
_numberOfDoors = State(wrappedValue: 0)
_multiOptions = State(wrappedValue: [String](repeating: "", count: 1))
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
The issue is, that when you initialize ContentView, the #State or #StateObject variables don't yet exist. You first need to initialize the View, before you can assign an #State variable.
An #State variable is wrapped in a property wrapper, where the original variable at initialization is _variable. Therefore, you need to initialize an #State variable like this:
_myVariable = State(wrappedValue: myContent)
If you don't so it is like this, you get the error as you mentioned.

Sharing Data between Views in Swift/better approach for this?

I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄

how to bind fileurls to NavigationLink list SwiftUI [duplicate]

This question already has answers here:
#Binding and ForEach in SwiftUI
(5 answers)
Closed 2 years ago.
I am trying to bind fileurls array to NavigationLink list I have an error
Cannot convert value of type 'Binding<[URL]>' to expected argument
type 'Range'
struct ContentView: View {
#State private var fileUrls:[URL] = []
func listDir(dir: String) -> [URL] {
// list file from directory
}
var body: some View {
NavigationView {
ZStack {
VStack {
List{
ForEach($fileUrls) { object in
NavigationLink(destination: PDFKitView(url: self.fileUrl1), isActive: $viewLocalPDF) {
Button("Swift Style"){
self.viewLocalPDF = true
}.padding(.bottom, 20)
}
}
}
}
.navigationBarTitle("Bookshelv ", displayMode: .inline).onAppear {
fileUrls = listDir(dir: "pdfs")
}
}
}
}
By using the $ you're referring to the Binding of fileUrls. You don't need the Binding for the ForEach, just the value.
Because URL doesn't conform to Identifiable, you'll also need to tell it how to identify each item. For this, you can just use \.self:
ForEach(fileUrls, id: \.self) { object in

SwiftUI List selection has no value

I want to basically make didSelectRow like UITableView in SwiftUI.
This is the code:
struct ContentView: View {
var testData = [Foo(name: "1"),
Foo(name: "2"),
Foo(name: "3"),
Foo(name: "4"),
Foo(name: "5")]
#State var selected: Foo?
var body: some View {
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
}
}
I was thought if I click the cell then selected should contains the selected value, but it's not. The selected has no value. And the cell not clickable.
So I added a Button.
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
Button("Test") {
print("\(foo) is selected.")
print(selected?.name)
}
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
Now, click works, but actually foo is the one I want there's no need selected why selection of the List is here.
Not sure anything I missed. Should the Button is necessary for the List "didSelectRow"? thanks!
EDIT
After a bit more investigation, my current conclusion is:
For single selections, no need call List(.. selection:). But you have to use Button or OnTapGesture for clickable.
List(.. selection:) is only for edit mode, which is multiple selection, as you can see the selection: needs a set. My example should be
#State var selected: Set<Foo>?
On iOS selection works in Edit mode by design
/// Creates a list with the given content that supports selecting multiple
/// rows.
///
>> /// On iOS and tvOS, you must explicitly put the list into edit mode for
>> /// the selection to apply.
///
/// - Parameters:
/// - selection: A binding to a set that identifies selected rows.
/// - content: The content of the list.
#available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content)
so you need either add EditButton somewhere, or activate edit mode programmatically, like
List(selection: $selection) {
// ... other code
}
.environment(\.editMode, .constant(.active)) // eg. persistent edit mode
Update: Here is some demo of default SwiftUI List selection
struct DemoView: View {
#State private var selection: Set<Int>?
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List(selection: $selection) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
}.onDelete(perform: {_ in})
}
.environment(\.editMode, .constant(.active))
}
}

Resources