How to know which button is selected in SwiftUI? - swift

I want to create 3 buttons. When I press on one button, I want that to be expand it with the x button. But instead of that, nothing happens. What am I doing wrong ? I will paste the code below.
This is the reutilizabile button I've created.
struct RCHChipView: View {
var rchChip: RCHChip
var action: () -> Void
init(
rchChip: RCHChip,
action: #escaping () -> Void) {
self.rchChip = rchChip
self.action = action
}
var body: some View {
HStack(spacing: 0) {
Button {
action()
} label: {
Text(rchChip.title)
}
if rchChip.isSelected {
Button {
rchChip.isSelected = false
} label: {
Image("NavigationCloseIcon")
.resizable()
}
}
}
}
}
struct RCHChip: Identifiable {
var id: String
var title: String
#Binding var isSelected: Bool
init(id: String = UUID().uuidString, title: String, isSelected: Binding<Bool> = .constant(false)) {
self.id = id
self.title = title
self._isSelected = isSelected
}
}
Here, I will show you how I'm trying to display the buttons.
#State private var rchChip: [RCHChip] = [RCHChip(title: "Datum"), RCHChip(title: "Person")]
HStack {
ForEach(rchChip) { chip in
RCHChipView(rchChip: rchChip.first(where: {$0.id == chip.id}) ?? RCHChip(id: "", title: "", isSelected: .constant(false))) {
chip.isSelected = true
}
}
}
How can I solve this issue ?

Your code is quite strange and I didn't really understand all of it but here is a solution that seems to work
First of all, you cannot use #Binding outside a View so I changed the struct to
struct RCHChip: Identifiable {
var id: String
var title: String
var isSelected: Bool
init(id: String = UUID().uuidString, title: String, isSelected: Bool = false) {
self.id = id
self.title = title
self.isSelected = isSelected
}
}
In the RCHChipView I found the definition of action strange since it didn't take any input nor returned anything. I changed it to the have the following signature
var action: (RCHChip) -> RCHChip
and to be able to modify a view property from outside of the view it needs to be a #Bindning so I changed rchChip to
#Binding var rchChip: RCHChip
the execution of the action property was then changed to
Button {
self.rchChip = action(rchChip)
}
And for the calling view I used the following code
#State private var rchChips: [RCHChip] = [RCHChip(title: "Datum"), RCHChip(title: "Person")]
var body: some View {
HStack {
ForEach($rchChips) { chip in
RCHChipView(rchChip: chip, action: { chip in
var copy = chip
copy.isSelected = true
return copy
})
}
}
}
I am sure this can be improved upon but it should be a step forward compared to your current solution and it does compile and run :)

I think this is to do with the behaviour of Binding variables.
Try:
struct RCHChipView: View {
var rchChip: RCHChip
var action: () -> Void
init(
rchChip: RCHChip,
action: #escaping () -> Void) {
self.rchChip = rchChip
self.action = action
}
var body: some View {
HStack(spacing: 0) {
Button {
action()
} label: {
Text(rchChip.title)
}
if rchChip.isSelected {
Button {
rchChip.isSelected.wrappedValue = false
} label: {
Image("NavigationCloseIcon")
.resizable()
}
}
}
}
}
The only difference being using the .wrappedValue for the Binding variable. Here's a slight change to the ForEach:
ForEach($rchChip) { $chip in
RCHChipView(
rchChip: chip,
action: { $chip.wrappedValue.isSelected = true }
)
}
$ is how you refer to Binding variables.

Related

SwiftUI toggles not changing when clicked on

I'm trying to build a simple SwiftUI view that displays a number of toggles. While I can get everything to display ok, I cannot get the toggles to flip. Here's is a simplified code example:
import SwiftUI
class StoreableParam: Identifiable {
let name: String
var id: String { name }
#State var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
init(name: String, isEnabled: Bool, toggleAction: ((Bool) -> Void)? = nil) {
self.name = name
self.isEnabled = isEnabled
self.toggleAction = toggleAction
}
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach(viewModel.storeParams, id: \.id) { storeParam in
Toggle(storeParam.name, isOn: storeParam.$isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}.navigationBarTitle("Toggle Example")
}
}
As mentioned in the comments, there are a couple of things going on:
#State is only for use in a View
Your model should be a struct
Then, you can get a Binding using the ForEach element binding syntax:
struct StoreableParam: Identifiable {
let name: String
var id: String { name }
var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach($viewModel.storeParams, id: \.id) { $storeParam in
Toggle(storeParam.name, isOn: $storeParam.isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}
}
}

Append array replaces items

I'm trying to append an array on a tap gesture, but it seems that it replaces any item in the array with the current one.
The Array and functions:
class ChipsViewModel: ObservableObject {
#Published var selectedCategories = [String]()
func appendArray(title:String) {
selectedCategories.append(title)
}
func removeFromArray(title:String){
if let index = selectedCategories.firstIndex(of: title) {
selectedCategories.remove(at: index)
}
}
}
The button itself:
#ObservedObject var viewModel = ChipsViewModel()
let systemImage: String
let titleKey: String
#State var isSelected: Bool
var body: some View {
HStack {
...
}.onTapGesture {
isSelected.toggle()
if(isSelected){
viewModel.appendArray(title: titleKey)
} else if (!isSelected){
viewModel.removeFromArray(title: titleKey)
}
print(viewModel.selectedCategories)
}
}
Not really sure why is this happening, it worked when i implemented a binding foreach, but using that setup the chips don't work as needed.
It looks like you are trying to keep state in sync in two places at the same time, it is usually not a good idea. It is best to keep it your view model
class ChipsViewModel: ObservableObject {
#Published var selectedCategories = [String]()
func isSelected(_ title: String) -> Bool {
selectedCategories.contains(title)
}
func toggleSelection(_ title: String) {
if let index = selectedCategories.firstIndex(of: title) {
selectedCategories.remove(at: index)
} else {
selectedCategories.append(title)
}
}
}
and then your View gets simpler and the state (i.e. whether a title is selected or not) cannot get out of sync:
#StateObject var viewModel = ChipsViewModel()
let systemImage: String
let titleKey: String
var isSelected: Bool {
viewModel.isSelected(titleKey)
}
var body: some View {
HStack {
//...
}
.onTapGesture {
viewModel.toggleSelection(titleKey)
}
}
The State needs to be initialized, like
let systemImage: String
let titleKey: String
#State var isSelected: Bool = false // << here !!

SwiftUI - multiple levels deep NavigationLink does not work

So I thought I found a way to make navigation in SwiftUI flexible and loosely coupled, yet still state-based and somewhat free of imperative-navigation bugs (double push, etc).
Basic idea is to have a linked list of Views (erased to AnyView) and a recursive view with NavigationLink in it, which is active when corresponding view is present in the list
But it does not work and I don't understand why. On iOS device it only pushes one level deep, even though the list is multiple levels deep and the bindings return true
Is it a SwiftUI bug or am I missing something?
struct ContentView: View {
#State
var navigationList: NavigationList?
var body: some View {
NavigationView {
Navigatable(list: $navigationList) {
Button("Push test", action: {
navigationList = .init(next: nil, screen: Screen {
TestView()
})
})
}
}
}
}
struct TestView: View {
#Environment(\.navigationList)
#Binding
var list
var body: some View {
Button("Push me", action: {
list = .init(next: nil, screen: Screen {
TestView()
})
})
}
}
struct Navigatable<Content: View>: View {
#Binding
var list: NavigationList?
let content: () -> Content
init(list: Binding<NavigationList?>, #ViewBuilder content: #escaping () -> Content) {
self._list = list
self.content = content
}
var body: some View {
ZStack {
NavigationLink(
isActive: isActive,
destination: {
Navigatable<Screen?>(list: childBinding) {
list?.screen
}
},
label: EmptyView.init
).hidden()
LazyView {
content()
}.environment(\.navigationList, $list)
}
}
var isActive: Binding<Bool> {
.init(
get: { list != nil },
set: {
if !$0 {
list = nil
}
}
)
}
var childBinding: Binding<NavigationList?> {
.init(
get: { list?.next },
set: { list?.next = $0 }
)
}
}
struct Screen: View {
let content: () -> AnyView
init<C: View>(#ViewBuilder content: #escaping () -> C) {
self.content = {
.init(content())
}
}
var body: some View {
content()
}
}
struct NavigationList {
#Indirect
var next: NavigationList?
let screen: Screen
}
enum NavigationListKey: EnvironmentKey {
static var defaultValue: Binding<NavigationList?> {
.constant(nil)
}
}
extension EnvironmentValues {
var navigationList: Binding<NavigationList?> {
get { self[NavigationListKey.self] }
set { self[NavigationListKey.self] = newValue }
}
}
struct LazyView<Content: View>: View {
#ViewBuilder var content: () -> Content
var body: some View {
content()
}
}
#propertyWrapper
struct Indirect<Wrapped> {
private final class Storage: CustomReflectable {
var wrapped: Wrapped
init(_ wrapped: Wrapped) {
self.wrapped = wrapped
}
var customMirror: Mirror {
.init(self, children: [(label: "wrapped", value: wrapped)])
}
}
private let storage: Storage
var wrappedValue: Wrapped {
get { storage.wrapped }
mutating set { storage.wrapped = newValue }
}
init(wrappedValue: Wrapped) {
self.storage = .init(wrappedValue)
}
}
You’re missing isDetailLink(false) which is what allows multiple screens to be pushed on to one navigation controller.
But there are also structural problems with the code. It's best to use SwiftUI View data structs as designed and let them store the hierachy of data. If you go off on your own architecture then you lose the magic like invalidation and diffing and it'll likely slow down too.

Passing A Closure With Arguments To SwiftUI View

I am working on a SwiftUI project. I've created a custom button that I can pass a function to. This looks like the following.
Custom Button
struct CustomButton: View {
let buttonTitle: String
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
In the view that uses this I can do the following.
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: myFunc)
}
}
func myFunc() {
print("My Custom Button Tapped")
}
This works really well.
What I want to do now is pass a parameter to the function. And I am having trouble with this. I tried the following.
struct CustomButton: View {
let buttonTitle: String
var function: (String) -> Void
var body: some View {
Button(action: {
self.function() // I DON'T KNOW WHAT DO TO HERE.
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: myFunc(text: "Hello"))
}
}
func myFunc(text: String) {
print(text)
}
This does not work. When I call CustomButton I get the following error.
Cannot convert value of type '()' to expected argument type '() ->
Void'
I also do not know what parameter to add to the self.function() call in the Button action.
Any help would be greatly appreciated.
First, the simplest answer -- by enclosing myFunc(text: "Hello") in { }, you can turn it into a closure. Then, it can get passed to your original () -> Void declaration.
struct CustomButton: View {
let buttonTitle: String
let function : () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: {
myFunc(text: "Hello")
})
}
}
You could also use an #autoclosure to provide similar behavior without the { }, but you'd have to declare a custom init for your CustomButton:
struct CustomButton: View {
let buttonTitle: String
let function : () -> Void
init(buttonTitle: String, function: #autoclosure #escaping () -> Void) {
self.buttonTitle = buttonTitle
self.function = function
}
var body: some View {
Button(action: {
self.function()
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: myFunc(text:"Hello"))
}
}
Finally, another option (that I think there's unlikely to a use case for, but just in case it fits) would be to pass the string parameter separately:
struct CustomButton: View {
let buttonTitle: String
let stringParameter : String
let function : (String) -> Void
var body: some View {
Button(action: {
self.function(stringParameter)
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", stringParameter: "Hello", function: myFunc)
}
}
Here what you may looking for:
struct ContentView: View {
let action: (String) -> Void = { value in print(value) }
var body: some View {
CustomButtonView(string: "print", valueToSend: "Hello World!", action: action)
}
}
struct CustomButtonView: View {
let string: String
let valueToSend: String
let action: (String) -> Void
var body: some View {
Button(string) {
action(valueToSend)
}
}
}

Handle single click and double click while updating the view

I'm trying to implement a single and double click for grid view items on macOS. On the first click it should highlight the item. On the second click it should open a detail view.
struct MyGridView: View {
var items: [String]
#State var selectedItem: String?
var body: some View {
LazyVGrid(columns: [
GridItem(.adaptive(minimum: 200, maximum: 270))
], alignment: .center, spacing: 8, pinnedViews: []) {
ForEach(items, id: \.self) { item in
Text("Test")
.gesture(TapGesture(count: 2).onEnded {
print("double clicked")
})
.simultaneousGesture(TapGesture().onEnded {
self.selectedItem = item
})
.background(self.selectedItem == item ? Color.red : .blue)
}
}
}
Problem
The highlighting works as expected without delay. But because the first click updates the view, the TapGesture for the double click is ignored. Only once the item was selected before, the double click also works, because it doesn't need to update the view again.
I was following https://stackoverflow.com/a/59992192/1752496 but it doesn't consider view updates after the first click.
You can use a custom click handler like this:
class TapState {
static var taps: [String: Date] = [:]
static func isDoubleTap<Content: View>(for view: Content) -> Bool {
let key = "\(view)"
let date = taps[key]
taps[key] = Date()
if let date = date, date.timeIntervalSince1970 >= Date().timeIntervalSince1970 - 0.4 {
return true
} else {
return false
}
}
}
extension View {
public func onTapGesture(firstTap: #escaping () -> Void, secondTap: #escaping () -> Void) -> some View {
onTapGesture(count: 1) {
if TapState.isDoubleTap(for: self) {
secondTap()
} else {
firstTap()
}
}
}
}
And implement it like this:
struct MyView: View {
#State var isSelected: Bool = false
#State var isShowingDetail: Bool = false
var body: some View {
Text("Text")
.background(isSelected ? Color.green : Color.blue)
.onTapGesture(firstTap: {
isSelected = true
}, secondTap: {
isShowingDetail = true
})
.sheet(isPresented: $isShowingDetail) {
Text("Detail")
}
}
}
Swift Package
I've also created a small package you can use: https://github.com/lukaswuerzburger/DoubleTap
As far as I know there is no build in function or something like that to understand deference between singleTap or doubleTap, but we have endless freedom to build what we want, I found this way:
You can apply tapRecognizer to anything you want, will work the same! for showing the use case I just applied to a Circle().
import SwiftUI
struct ContentView: View {
var body: some View {
Circle()
.fill(Color.red)
.frame(width: 100, height: 100, alignment: .center)
.tapRecognizer(tapSensitivity: 0.2, singleTapAction: singleTapAction, doubleTapAction: doubleTapAction)
}
func singleTapAction() { print("singleTapAction") }
func doubleTapAction() { print("doubleTapAction") }
}
struct TapRecognizerViewModifier: ViewModifier {
#State private var singleTapIsTaped: Bool = Bool()
var tapSensitivity: Double
var singleTapAction: () -> Void
var doubleTapAction: () -> Void
init(tapSensitivity: Double, singleTapAction: #escaping () -> Void, doubleTapAction: #escaping () -> Void) {
self.tapSensitivity = tapSensitivity
self.singleTapAction = singleTapAction
self.doubleTapAction = doubleTapAction
}
func body(content: Content) -> some View {
return content
.gesture(simultaneouslyGesture)
}
private var singleTapGesture: some Gesture { TapGesture(count: 1).onEnded{
singleTapIsTaped = true
DispatchQueue.main.asyncAfter(deadline: .now() + tapSensitivity) { if singleTapIsTaped { singleTapAction() } }
} }
private var doubleTapGesture: some Gesture { TapGesture(count: 2).onEnded{ singleTapIsTaped = false; doubleTapAction() } }
private var simultaneouslyGesture: some Gesture { singleTapGesture.simultaneously(with: doubleTapGesture) }
}
extension View {
func tapRecognizer(tapSensitivity: Double, singleTapAction: #escaping () -> Void, doubleTapAction: #escaping () -> Void) -> some View {
return self.modifier(TapRecognizerViewModifier(tapSensitivity: tapSensitivity, singleTapAction: singleTapAction, doubleTapAction: doubleTapAction))
}
}