SwiftUI - Navigation Link opening the same view multiple times - ios

I have a list in ViewA which has 3 rows. ViewA is a grand-child of the main view(not sure if this info matters, but just providing since I don't know where the actual bug is. Here is how the stack is ContentView -> opens Main Menu View(MMV) -> select an item from MMV and it opens -> Settings View -> select an item from Settings and it opens ViewA).
When I click on a row in ViewA, another view should opens up let's say ViewB. However I am seeing ViewB instantiating 3 times i.e. I see ViewB appearing 3 times on the screen and then it stops at the last instance.
I am not sure why this is happening and what the actual bug is. Here is my code:
ViewA(MenuViewOptions):
struct MenuViewOptions: View {
#State var destination: MenuViewType?
var isActive: Binding<Bool> { Binding(get: { destination != nil }, set: { _ in destination = nil } ) }
var body: some View {
VStack {
List(SettingsOptions().menuViewSettingsOptions) { item in
NavigationLink(isActive: isActive, destination: {destination?.view} ) {
MenuViewOptionRowView(menuViewItem: item, destination: $destination)
}
}
}.navigationBarTitle("Settings")
}
}
MenuViewType code:
enum MenuViewType: Int, CaseIterable, Hashable {
case circularGrid = 0, regularList = 1, capsuleGrid = 2
#ViewBuilder var view: some View {
switch self {
case .circularGrid: MainMenuCircularGridViewNew()
case .regularList: MainMenuListView()
case .capsuleGrid: MainMenuCapsuleGridView()
}
}
}
SettingsOptions().menuViewSettingsOptions code:
struct SettingsOptions {
// View inside menu view settings
let menuViewSettingsOptions: [MenuViewSettingsItem] = [
MenuViewSettingsItem(id: 0, name: "Circular Grid", imageName: "circle.grid.2x2", isSelected: false),
MenuViewSettingsItem(id: 1, name: "List", imageName: "list.dash", isSelected: false),
MenuViewSettingsItem(id: 2, name: "Capsule Grid", imageName: "rectangle.grid.2x2", isSelected: false),
]
}
MenuViewOptionRowView code
struct MenuViewOptionRowView: View {
let menuViewItem: MenuViewSettingsItem
#ObservedObject var userSettingsStore = UserSettingsStore()
#Binding var destination: MenuViewType?
var body: some View {
HStack {
Image(systemName: menuViewItem.imageName)
.circularImageStyle(width: 15, height: 15, padding: 5, backgroundColor: Color(red: 34 / 255, green: 34 / 255, blue: 34 / 255), foregroundColor: Color(red: 23 / 255, green: 121 / 255, blue: 232 / 255))
Text(menuViewItem.name)
Spacer()
if menuViewItem.id == userSettingsStore.menuViewSelection {
Image(systemName: "checkmark").foregroundColor(Color(red: 23 / 255, green: 121 / 255, blue: 232 / 255))
}
}.onTapGesture {
userSettingsStore.menuViewSelection = self.menuViewItem.id
destination = MenuViewType(rawValue: self.menuViewItem.id)
}
}
}
Basically I am setting the destination in MenuViewOptionRowView and once it's set, isActive in MenuViewOptions becomes true and NavigationLink gets fired. I am seeing the row navigates to the correct view, but just that the view instantiates 3 times which I am not sure if it is because I have 3 rows in the list.
If you have any idea of what could be the issue, please do let me know. New to SwiftUI.
Thanks!

You set destination once, but with this you activate all links in List, because they are all bound to one isActive.
With such code design I would propose the following solution: one destination - one link, destination is already set on tap, so... (code in question is not testable so just as-is)
List(SettingsOptions().menuViewSettingsOptions) { item in
MenuViewOptionRowView(menuViewItem: item, destination: $destination)
}
.background(
NavigationLink(isActive: isActive, destination: {destination?.view} ) {
EmptyView()
}
)

Related

swift ColorPicker is triggered twice even when one color is selected only once

I need to create an array of colors created/selected by the user by using ColorPicker, bind it to a #State var tempColor variable, and append tempColor to the array of selected colors, but the problem is the behavior is unstable. Specifically, sometimes the same color is added twice to the array even when a color is selected/pressed once.
I added some logic to check for duplicate colors before adding the selected color to the array but still a color is added twice to the array. This behavior occurs like 80% of the time when ColorPicker is pressed and a color is selected.
Any idea to solve the problem please?
my code:
struct ContentView: View {
#State var createdColors = [Color]()
#State var tempColor: Color = .white
var body: some View {
VStack {
ColorPicker("selected color", selection: $tempColor)
.padding(10)
ScrollView(.horizontal) {
HStack {
ForEach(createdColors, id: \.self) { col in
Circle()
.fill(col)
.frame(width: 60, height: 60)
.padding(10)
}
}
}
}
.onChange(of: tempColor) { _ in
if createdColors.contains(tempColor) {
print("we have the color already \(createdColors.count)")
} else {
createdColors.append(tempColor)
print("adding new color \(createdColors.count)")
}
}
}
}
Give the system a little break.
struct ContentView: View {
#State var createdColors = [Color]()
#State var tempColor: Color = .white
var body: some View {
VStack {
ColorPicker("selected color", selection: $tempColor)
.padding(10)
Text("Currently ^[\(createdColors.count) item](inflect: true)")
ScrollView(.horizontal) {
HStack {
ForEach(createdColors, id: \.self) { col in
Circle()
.fill(col)
.frame(width: 60, height: 60)
.padding(10)
}
}
}
}
.onChange(of: tempColor) { newColor in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if createdColors.contains(newColor) {
print("We have the \(newColor.description) already. Total: \(createdColors.count)")
} else {
createdColors.append(tempColor)
print("Adding new color to \(createdColors.count)")
}
}
}
}
}
There are better ways to resolve the issue. This is just a quick fix.

Access a variable in another view to reset it

I will try to explain my project, I develop an app on iPad.
First I have an HStack with a series of buttons which contain data, the button are green at the beginning and when we tap on it the button becomes red and the data contained in the button are stored in a variable.
Then, I have a second series of buttons which contain initial data, which must be replaced by other data (the data contained in the variable when we tap on a button which is in the HStack).
So we tap on the first button the data is recorded in a variable and we tap on a second button where we want the data.
The problem is that after that I want to delete the button contained in HStack this I can do it. But the red color stay on another button.
I suppose that it is because the "didTap" variable remains at true but how Can I "reset" this variable (becomes again false) when I tap on the second button.
Here is a simplified version of the code :
import SwiftUI
struct ContentView: View {
#State private var allWords : [String] = ["test1", "test2", "test3", "test4", "test5", "test6"] // contain the data to put in the buttons which are in the HStack
#State private var textInButton : String = "" //text temporarly stored
#State private var arrayIndice : Int = 0 //index temporarly stored
#State private var oneTap : Bool = false
#State private var isNight = false //unused at this moment
private var col : [GridItem] = Array(repeating: .init(.fixed(45)), count: 18)
let spaceOriginal = ["A1","A2","B1"]
#State var space = ["A1","A2","B1"]
var body: some View {
VStack{
ScrollView(.horizontal) {
HStack{
ForEach(allWords.indices, id: \.self) { index in
// display list of buttons ...
MyButton(myRow: allWords[index], oneTap: $oneTap, isNight: $isNight, textInButton: $textInButton, arrayIndice: $arrayIndice, arrayLocalIndice : index, allWords: $allWords)
}
}
.padding(.bottom, 30)
}
LazyVGrid(columns: col, spacing: 0) {
ForEach(0..<space.count) { i in
// display some button that I want to fill with the data contained in the buttons which are in the HStack
Button(action: {
// display again the original data when we click on the button and want to "reset" the button
if (space[i] != spaceOriginal[i]){
allWords.append(space[i])
space[i] = spaceOriginal[i]
}
// put data which are in the selected button (located in the hstack) in the button which I have to fill
if(textInButton != "" && space[i] == spaceOriginal[i]){
space[i] = textInButton
textInButton = ""
allWords.remove(at: arrayIndice)
oneTap = false
}
}, label: {
Text("\(space[i])")
.frame(width:50, height:75)
.font(.system(size: 20, weight: .medium))
.border(Color.black, width: 1)
})
}
}
}
}
}
struct ULD: View{
var title: String
var textColor: Color
var backgroundColor: Color
var body: some View{
Text(title)
.frame(width:75, height:75)
.background(backgroundColor)
.font(.system(size: 10, weight: .medium))
.foregroundColor(textColor)
.border(Color.black, width: 1)
}
}
struct MyButton: View {
#State private var didTap = false
var myRow: String
#Binding var oneTap: Bool
#Binding var isNight: Bool
#Binding var textInButton: String
#Binding var arrayIndice: Int
var arrayLocalIndice : Int
#Binding var allWords: [String]
var body: some View {
Button{
// reset the button if I tap again on a button located in the Hstack
if (didTap == true){
didTap.toggle()
oneTap = false
textInButton = ""
arrayIndice = 0
}
// tap on the button
else if (oneTap == false){
oneTap = true
didTap.toggle()
textInButton = myRow
arrayIndice = arrayLocalIndice
isNight.toggle()
}
} label:{
// By default the button is green, when we tap the first time on the button the didTap variable becomes true and the button becomes red, if we tap again it becomes green again
ULD(title: myRow, textColor: .black, backgroundColor: didTap ? .red : .green)
}
}
}
Here is some pictures to illustrate the problem :
Initial state :
When I click on test 2 it becomes red :
When I click on the position A2 the button is filled with test 2 but test 3 (the second position in the HStack remains red) :
Thank you very much

View body is called although its #Binding has not changed

SwiftUI promise is to call View’s body only when needed to avoid invalidating views whose State has not changed.
However, there are some cases when this promise is not kept and the View is updated even though its state has not changed.
Example:
struct InsideView: View {
#Binding var value: Int
// …
}
Looking at that view, we’d expect that its body is called when the value changes. However, this is not always true and it depends on how that binding is passed to the view.
When the view is created this way, everything works as expected and InsideView is not updated when value hasn’t changed.
#State private var value: Int = 0
InsideView(value: $value)
In the example below, InsideView will be incorrectly updated even when value has not changed. It will be updated whenever its container is updated too.
var customBinding: Binding<Int> {
Binding<Int> { 100 } set: { _ in }
}
InsideView(value: customBinding)
Can anyone explain this and say whether it's expected? Is there any way to avoid this behaviour that can ultimately lead to performance issues?
Here's a sample project if anyone wants to play with it.
And here's a full code if you just want to paste it to your project:
import SwiftUI
struct ContentView: View {
#State private var tab = 0
#State private var count = 0
#State private var someValue: Int = 100
var customBinding: Binding<Int> {
Binding<Int> { 100 } set: { _ in }
}
var body: some View {
VStack {
Picker("Tab", selection: $tab) {
Text("#Binding from #State").tag(0)
Text("Custom #Binding").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
VStack(spacing: 10) {
if tab == 0 {
Text("When you tap a button, a view below should not be updated. That's a desired behaviour.")
InsideView(value: $someValue)
} else if tab == 1 {
Text("When you tap a button, a view below will be updated (its background color will be set to random value to indicate this). This is unexpected because the view State has not changed.")
InsideView(value: customBinding)
}
}
.frame(width: 250, height: 150)
Button("Tap! Count: \(count)") {
count += 1
}
}
.frame(width: 300, height: 350)
.padding()
}
}
struct InsideView: View {
#Binding var value: Int
var body: some View {
print("[⚠️] InsideView body.")
return VStack {
Text("I'm a child view. My body should be called only once.")
.multilineTextAlignment(.center)
Text("Value: \(value)")
}
.background(Color.random)
}
}
extension ShapeStyle where Self == Color {
static var random: Color {
Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
Hmm i think the reason is being updated is because you are using a computed property in the ContentView view. and even if it's not tagged with a state annotation like #State, #Binding,#Stateobject... its a view state regardless, and swiftui use that to infer the difference between a view with an old state and a new state. And you're getting a new binding object at every contentview body update.
you can try change the init from what you have to something like this
let customBinding: Binding<Int>
init() {
self.customBinding = Binding<Int> { 99 } set: { _ in }
}
but i would advise against this approach because simply is not useful to create a binding like this in the view, because you can't change anything inside the set because it's the init of a struct.
Instead you can pass in the init an ObservableObject where you moved the state logic to an ObservableObject and use that.
something like this
class ContentViewState: ObservableObject {
#Published var someValue: Int = 100
var customBinding: Binding<Int> = .constant(0)
init() {
customBinding = Binding<Int> { [weak self] in
self?.someValue ?? 0
}
set: { [weak self] in
self?.someValue = $0
}
}
}
// and change the InsideView like
struct InsideView: View {
#ObservedObject private var state: ContentViewState
#Binding var value: Int
init(state: ContentViewState) {
self.state = state
_value = state.customBinding
}
...
}
I would still use the $ with simple #state notation most of the time where i don't have complicated states to handle, but this can be another approach i guess.

Buttons in NavigationViewItems do not work with incomplete swipe

I created a NavigationView which I put a NavigationLink on to another View. The first page with a hidden title. After going to page 2 through Link, everything works at first. I have a title there and 2 navigationBarItems buttons. But if you pull from left to right back to the first screen, but then return (that is, do not go, but only pull a little), then all the buttons stop working. What is the problem?
struct View1: View {
var body: some View {
NavigationView {
ZStack {
Color.red
NavigationLink("View 2", destination: View2())
}.navigationbarhidden(true)
}
}
}
struct View2: View {
#State private var texti: String = ""
#State var themeColor = Color.green
var body: some View {
VStack {
TextEditor(text: $texti).padding().onReceive(texti.publisher.collect()) {
self.texti = String($0.prefix(2000))}
}.navigationBarTitle("Title").navigationBarItems(trailing: HStack { Button(action:{}) {
Text("Fund").foregroundColor(Color(#colorLiteral(red: 0.3647058824, green: 0.6901960784, blue: 0.4588235294, alpha: 1)))
}; Button(action: {}) {
Text("Gust").foregroundColor(Color(#colorLiteral(red: 0.3647058824, green: 0.6901960784, blue: 0.4588235294, alpha: 1)))
}})
}

Swap two elements onTapGesture with zIndex updated for the animated transition

I use matchedGeometryEffect and TapGesture to swap two elements with animated transition.
The animation works as expected but I want to change the zIndex of the elements for the duration of the animation.
I started from this example : https://www.appcoda.com/matchedgeometryeffect/
and adapted it to my needs :
struct MyView: View {
#State var colors = [Color.orange,Color.red,Color.yellow,Color.pink,Color.blue]
#State var target = Int.random(in: 0..<4)
#Namespace private var animationNamespace : Namespace.ID
var body: some View {
let columns: [GridItem] = Array(repeating: .init(.fixed(80)), count: 2)
LazyVGrid(columns: columns, spacing: 5){
ForEach(Array(self.colors.enumerated()), id: \.self.element) { indexedColor in
Element(color: indexedColor.element, animationNamespace: self.animationNamespace, action: {
self.colors.swapAt(target, indexedColor.offset)
target = Int.random(in: 0..<4)
})
}
}.animation(.linear)
}
}
struct Element: View {
let color : Color
let animationNamespace : Namespace.ID
let action: () -> Void
var body: some View {
Circle()
.fill(color)
.frame(width: 80, height: 80)
.matchedGeometryEffect(id: color, in: animationNamespace)
.onTapGesture {
action()
}
}
}
I want the animated elements to be always on top of the other elements.
The zIndex must be reseted at the end of the animation
Is this possible by using TapGesture ?
Xcode Version 12.0.1 (12A7300)
iOS 14.0
It is possible to put the clicked dot on the top level.. I changed your code here..
struct ContentView: View {
#State var colors = [Color.orange,Color.red,Color.yellow,Color.pink,Color.blue]
#Namespace private var animationNamespace : Namespace.ID
var body: some View {
HStack{
ForEach(Array(self.colors.enumerated()), id: \.self.element) { indexedColor in
Element(color: indexedColor.element, animationNamespace: self.animationNamespace, order: indexedColor.0, action: {
self.colors.swapAt(0, indexedColor.offset)
})
}
}.animation(.linear)
}
}
struct Element: View {
let color : Color
let animationNamespace : Namespace.ID
var order : Int
let action: () -> Void
var body: some View {
Circle()
.fill(color)
.frame(width: 80, height: 80)
.matchedGeometryEffect(id: color, in: animationNamespace)
.zIndex(Double(5-order))
.onTapGesture {
action()
}
}
}
Basically I just put the zIndex for each element based on their position. This means the first dot will have the highest zIndex. If you change the positions, the clicked one will get the highest zIndex and will lay on top of each other. I think it is not possible to put the other one on top aswell, as it will get the other zIndex, which was the lower one.

Resources