Passing A Closure With Arguments To SwiftUI View - swift

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)
}
}
}

Related

How to know which button is selected in SwiftUI?

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.

Is there a way to know if a view has already been loaded in SwiftUI

I'm trying to focus the isUsernameFocused textField as soon as it loads on the screen, I tried doing it directly in the onAppear method but it looks like it needs a delay in order for it to focus. My concern is that for some reason the focus only occurs with a delay greater than 0.6 fractions of a second. Setting it at 0.7 fractions of a second seems to work fine but I'm afraid that eventually, this will stop working if the view gets bigger since it will need more time to load.
Is there a way to know when the VStack is fully loaded so I can trigger the isUsernameFocused? Something like, viewDidLoad in UIKit.
struct ContentView: View {
#FocusState private var isUsernameFocused: Bool
#State private var username = ""
var body: some View {
VStack {
TextField("Username", text: $username)
.focused($isUsernameFocused)
}
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7){
self.isUsernameFocused = true
}
}
}
}
If you are on macOS or tvOS you can use prefersDefaultFocus for that. It should come to iOS in June.
In the meantime, I just created this example that works around the issue. If your form appears multiple times you might want to check other values before setting the focus.
import SwiftUI
import UIKit
struct FocusTestView : View {
#State var presented = false
var body: some View {
Button("Click Me") {
presented = true
}
.sheet(isPresented: $presented) {
LoginForm()
}
}
}
struct LoginForm : View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
// handleLogin(username, password)
}
}
}
.uiKitOnAppear {
focusedField = .usernameField
}
}
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.action = action
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
}
}
class UIAppearViewController: UIViewController {
var action: () -> Void = {}
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
DispatchQueue.main.asyncAfter(deadline:.now()) { [weak self] in
self?.action()
}
}
}
public extension View {
func uiKitOnAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
UIKitAppear was taken from dev forum post and I added the dispatch async to call the action. LoginForm is from the docs on FocusState.

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.

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))
}
}

How to implement custom callback action in SwiftUI? Similar to onAppear functionality

I have custom ActionView with two buttons: Car and Bike. When these buttons tapped I need that in the MainView modifiers onCarTap/onBikeTap will be triggered.
With my current implementation here is error:
Argument passed to call that takes no arguments
Value of tuple type 'Void' has no member 'onBikeTap'
Source code:
struct ActionView: View {
// Callback for button taps
var onCarTap: (() -> Void)?
var onBikeTap: (() -> Void)?
var body: some View {
HStack {
Button(action: {
onCarTap?()
}, label: {
Text("Car")
})
Button(action: {
onBikeTap?()
}, label: {
Text("Bike")
})
}
}
}
I am looking for solution like this:
struct MainView: View {
var body: some View {
ActionView()
.onCarTap({})
.onBikeTap({ })
}
}
It is possible to implement in this way:
ActionView(onCarTap: {
print("on car tap")
}, onBikeTap: {
print("on bike tap")
})
Assuming you have the following View:
struct ActionView: View {
var onCarTapAction: (() -> Void)?
var onBikeTapAction: (() -> Void)?
var body: some View {
HStack {
Button(action: {
onCarTapAction?()
}, label: {
Text("Car")
})
Button(action: {
onBikeTapAction?()
}, label: {
Text("Bike")
})
}
}
}
You can create an extension:
extension ActionView {
func onCarTap(action: #escaping (() -> Void)) -> ActionView {
ActionView(onCarTapAction: action, onBikeTapAction: onBikeTapAction)
}
func onBikeTap(action: #escaping (() -> Void)) -> ActionView {
ActionView(onCarTapAction: onCarTapAction, onBikeTapAction: action)
}
}
and use it like this:
struct ContentView: View {
var body: some View {
ActionView()
.onCarTap {
print("onCarTap")
}
.onBikeTap {
print("onBikeTap")
}
}
}
You can declare a modifier for your purpose like the following.
extension ActionView {
func onCarTap(_ handler: #escaping () -> Void) -> ActionView {
var new = self
new.onCarTap = handler
return new
}
}
In addition, if you prefer to hide the handler property with private or fileprivate to prevent to be accessed directly, have to declare a designated init which accepts parameters for its properties except one for the handler.