How to change position of SearchBar in NavigationBar? - swift

I followed this article on how to display a SearchBar in the NavigationBar. I integrated it like this into my view:
struct ExploreView: View {
#ObservedObject var searchBar = SearchBar()
var body: some View {
NavigationView {
ZStack {
Color(red: 250/255, green: 250/255, blue: 250/255)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
Image(R.image.navigationBarBackground)
.resizable()
.scaledToFit()
.frame(width: UIScreen.main.bounds.width)
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
.navigationBarTitle("", displayMode: .inline)
.add(self.searchBar)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {
}
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
It look's like this: SearchBar: inactive, SearchBar: active
But I would like to have the inactive SearchBar in the same position as the active SearchBar to avoid the free space. In the end it should look like the SearchBar in the Instagram App. Does anyone know how to do this?

Add this piece of code inside your init() method of SearchBar. It will make search bar at same position when it's active.
self.searchController.hidesNavigationBarDuringPresentation = false
If you want to set search bar to navigation bar title instead of text, inside your overlay(_:)
Change this code
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
To
ViewControllerResolver { viewController in
viewController.navigationItem.titleView = self.searchBar.searchController.searchBar
}

Related

how to use textField Extension to disable Copy and paste in swift ios

I am trying to disable copy and paste functionality in a textField with the help of swift without using any separate cocoatouch class.
Can you suggest how to do it using Either keyboard extension or textfield extension.
Any response will be appreciated.
Create a custom TextField as use as follows
import SwiftUI
struct CustomeTextField: View {
#State var textStr = ""
var body: some View {
VStack(spacing: 10) {
Text("This is textfield:")
.font(.body)
.foregroundColor(.gray)
TextFieldWrapperView(text: self.$textStr)
.background(Color.gray)
.frame(width: 200, height: 50)
}
.frame(height: 40)
}
}
struct TextFieldWrapperView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> TFCoordinator {
TFCoordinator(self)
}
}
extension TextFieldWrapperView {
func makeUIView(context: UIViewRepresentableContext<TextFieldWrapperView>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
class TFCoordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldWrapperView
init(_ textField: TextFieldWrapperView) {
self.parent = textField
}
func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == #selector(UIResponderStandardEditActions.paste(_:)) {
return false
}
return canPerformAction(action: action, withSender: sender)
}
}

How can I be able to controlling the blur amount in macOS?

I am able to control the blur amount in iOS with help of fractionComplete then I am trying to do same work in macOS native app, I made a multi platform project which run for iOS and native macOS, the codes are in below, the version of iOS works just fine and looking to reach same effect of controlling the amount of blur in macOS, but unable to find a property works as fractionComplete in macOS version.
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Text("Hello, world!")
.font(Font.system(size: 50.0))
.padding()
.background(Color.yellow)
.fixedSize()
Text("Hello, world!")
.font(Font.system(size: 25.0))
.foregroundColor(Color.blue)
.padding(20)
.background(UnicornView())
.border(Color.blue)
}
}
}
#if os(iOS)
struct UnicornView: UIViewControllerRepresentable {
class UnicornControllerModel: UIViewController {
var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
var visualEffectView = UIVisualEffectView(effect: nil)
override func viewDidLoad() {
super.viewDidLoad()
animator.addAnimations {
self.visualEffectView.effect = UIBlurEffect(style: .systemUltraThinMaterial)
}
animator.fractionComplete = 0.2
view = visualEffectView
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<UnicornView>) -> UnicornControllerModel {
return UnicornControllerModel()
}
func updateUIViewController(_ uiViewController: UnicornControllerModel, context: UIViewControllerRepresentableContext<UnicornView>) {
}
}
#endif
#if os(macOS)
struct UnicornView: NSViewControllerRepresentable {
class UnicornControllerModel: NSViewController {
var animator = NSAnimation(duration: 1, animationCurve: .linear)
var visualEffectView = NSVisualEffectView()
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
visualEffectView.material = .popover
visualEffectView.blendingMode = .withinWindow
view = visualEffectView
}
override func loadView() {
view = NSView()
}
}
func makeNSViewController(context: NSViewControllerRepresentableContext<UnicornView>) -> UnicornControllerModel {
return UnicornControllerModel()
}
func updateNSViewController(_ nsViewController: UnicornControllerModel, context: NSViewControllerRepresentableContext<UnicornView>) {
}
}
#endif

Hide navigation bar without losing swipe back gesture in SwiftUI

In SwiftUI, whenever the navigation bar is hidden, the swipe to go back gesture is disabled as well.
Is there any way to hide the navigation bar while preserving the swipe back gesture in SwiftUI? I've already had a custom "Back" button, but still need the gesture.
I've seen some solutions for UIKit, but still don't know how to do it in SwiftUI
Here is the code to try yourself:
import SwiftUI
struct RootView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View{
Text("As you can see, swipe to go back will not work")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Any suggestions or solutions are greatly appreciated
This should work by just extending UINavigationController.
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
It is even easier than what Nick Bellucci answered.
Here is the simplest working solution:
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = nil
}
}
When using the UINavigationController extension you might encounter a bug that blocks your navigation after you start swiping the screen and let it go without navigating back. Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView does fix this issue.
If you need different view styles based on device, this extension helps:
extension View {
public func currentDeviceNavigationViewStyle() -> AnyView {
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
} else {
return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
}
}
}
I looked around documentation and other sources about this issue and found nothing. There are only a few solutions, based on using UIKit and UIViewControllerRepresentable. I tried to combine solutions from this question and I saved swipe back gesture even while replacing back button with other view. The code is still dirty a little, but I think that is the start point to go further (totally hide navigation bar, for example). So, here is how ContentView looks like:
import SwiftUI
struct ContentView: View {
var body: some View {
SwipeBackNavController {
SwipeBackNavigationLink(destination: DetailViewWithCustomBackButton()) {
Text("Main view")
}
.navigationBarTitle("Standard SwiftUI nav view")
}
.edgesIgnoringSafeArea(.top)
}
}
// MARK: detail view with custom back button
struct DetailViewWithCustomBackButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("detail")
.navigationBarItems(leading: Button(action: {
self.dismissView()
}) {
HStack {
Image(systemName: "return")
Text("Back")
}
})
.navigationBarTitle("Detailed view")
}
private func dismissView() {
presentationMode.wrappedValue.dismiss()
}
}
Here is realization of SwipeBackNavController and SwipeBackNavigationLink which mimic NavigationView and NavigationLink. They are just wrappers for SwipeNavigationController's work. The last one is a subclass of UINavigationController, which can be customized for your needs:
import UIKit
import SwiftUI
struct SwipeBackNavController<Content: View>: UIViewControllerRepresentable {
let content: Content
public init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> SwipeNavigationController {
let hostingController = UIHostingController(rootView: content)
let swipeBackNavController = SwipeNavigationController(rootViewController: hostingController)
return swipeBackNavController
}
func updateUIViewController(_ pageViewController: SwipeNavigationController, context: Context) {
}
}
struct SwipeBackNavigationLink<Destination: View, Label:View>: View {
var destination: Destination
var label: () -> Label
public init(destination: Destination, #ViewBuilder label: #escaping () -> Label) {
self.destination = destination
self.label = label
}
var body: some View {
Button(action: {
guard let window = UIApplication.shared.windows.first else { return }
guard let swipeBackNavController = window.rootViewController?.children.first as? SwipeNavigationController else { return }
swipeBackNavController.pushSwipeBackView(DetailViewWithCustomBackButton())
}, label: label)
}
}
final class SwipeNavigationController: UINavigationController {
// MARK: - Lifecycle
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// This needs to be in here, not in init
interactivePopGestureRecognizer?.delegate = self
}
deinit {
delegate = nil
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
duringPushAnimation = true
setNavigationBarHidden(true, animated: false)
super.pushViewController(viewController, animated: animated)
}
var duringPushAnimation = false
// MARK: - Custom Functions
func pushSwipeBackView<Content>(_ content: Content) where Content: View {
let hostingController = SwipeBackHostingController(rootView: content)
self.delegate = hostingController
self.pushViewController(hostingController, animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension SwipeNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
}
// MARK: - UIGestureRecognizerDelegate
extension SwipeNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == interactivePopGestureRecognizer else {
return true // default value
}
// Disable pop gesture in two situations:
// 1) when the pop animation is in progress
// 2) when user swipes quickly a couple of times and animations don't have time to be performed
let result = viewControllers.count > 1 && duringPushAnimation == false
return result
}
}
// MARK: Hosting controller
class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.delegate = nil
}
}
This realization provides to save custom back button and swipe back gesture for now. I still don't like some moments, like how SwipeBackNavigationLink pushes view, so later I'll try to continue research.

SwiftUI inputAccesoryView Implementation

I am trying to implement an inputAccessoryView on a TextField in SwiftUI. The goal is to have a "Done" Button appear above the Keyboard which when pressed gets rid of the keyboard (i.e. resignFirstResponder()).
I came across the following Medium article which purports to implement this behavior exactly as I would require, however, I am struggling to get it working.
Medium link containing method to be implemented.
I have tried to implement this in a blank XCode project, my code compiles, however, the TextField never shows up, and I cannot touch in the area it should be to bring up the keyboard. How do I correctly implement this code to get the desired behavior?
Code
import Foundation
import UIKit
import SwiftUI
class TextFieldViewController
: UIViewController {
// our custom text field will report changes to the outside
let text: Binding<String>?
// if the toolbar (see below) is used (Done), the keyboard shall be dismissed
// and optionally we execute a provided closure
let onDismiss: (() -> Void)?
init (
text: Binding<String>
, onDismiss: (() -> Void)?) {
self.text = text
self.onDismiss = onDismiss
super.init(
nibName: nil //"<XIB>"
, bundle: nil //Bundle.main?
)
}
required init?(coder: NSCoder) {
self.text = nil
self.onDismiss = nil
super.init(coder: coder)
}
// helper function to encapsulate calling the "view" of UIViewController
fileprivate func getTextField() -> UITextField? {
return view as? UITextField
}
override func viewDidLoad() {
let textField = self.getTextField()
guard textField != nil else {
return
}
// configure a toolbar with a Done button
let toolbar = UIToolbar()
toolbar.setItems([
// just moves the Done item to the right
UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace
, target: nil
, action: nil
)
, UIBarButtonItem(
title: "Done"
, style: UIBarButtonItem.Style.done
, target: self
, action: #selector(self.onSet)
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
textField?.inputAccessoryView = toolbar
}
#objc private func onSet() {
let textField = self.getTextField()
textField?.resignFirstResponder()
self.text?.wrappedValue = textField?.text ?? ""
self.onDismiss?()
}
}
// The SwiftUI view, wrapping the UITextField
struct TextFieldView: View {
var text: Binding<String>
var onDismissKeyboard: (() -> Void)?
var body: some View {
TextFieldRepresentable(
text: self.text
, dismissKeyboardCallback: self.onDismissKeyboard
)
}
}
// The UIViewControllerRepresentable, feeding and controlling the UIViewController
struct TextFieldRepresentable
: UIViewControllerRepresentable {
// the callback
let dismissKeyboardCallback: (() -> Void)?
// created in the previous file/gist
let viewController: TextFieldViewController
init (
text: Binding<String>
, dismissKeyboardCallback: (() -> Void)?) {
self.dismissKeyboardCallback = dismissKeyboardCallback
self.viewController = TextFieldViewController(
text: text
, onDismiss: dismissKeyboardCallback
)
}
// UIViewControllerRepresentable
func makeUIViewController(context: Context) -> UIViewController {
return viewController
}
// UIViewControllerRepresentable
func updateUIViewController(_ viewController: UIViewController, context: Context) {
}
}
struct ContentView : View {
#State var email:String = ""
var body: some View {
HStack{
Circle()
TextFieldView(text: $email)
Circle()
}
}
}
Here is a demo with custom toolbar & binding for entered text, but simplified by excluding on dismiss callback (as it is not important for approach demo), just to have less code. Hope it will be helpful.
import SwiftUI
import UIKit
import Combine
struct CustomInputTextField : UIViewRepresentable {
#Binding var text: String
let textField = UITextField(frame: CGRect(x:0, y:0, width: 100, height: 32)) // just any
func makeUIView(context: UIViewRepresentableContext<CustomInputTextField>) -> UITextField {
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomInputTextField>) {
self.textField.text = text
}
func makeCoordinator() -> CustomInputTextField.Coordinator {
let coordinator = Coordinator(self)
// configure a toolbar with a Done button
let toolbar = UIToolbar()
toolbar.setItems([
// just moves the Done item to the right
UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace
, target: nil
, action: nil
)
, UIBarButtonItem(
title: "Done"
, style: UIBarButtonItem.Style.done
, target: coordinator
, action: #selector(coordinator.onSet)
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
textField.inputAccessoryView = toolbar
return coordinator
}
typealias UIViewType = UITextField
class Coordinator: NSObject {
let owner: CustomInputTextField
private var subscriber: AnyCancellable
init(_ owner: CustomInputTextField) {
self.owner = owner
subscriber = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: owner.textField)
.sink(receiveValue: { _ in
owner.$text.wrappedValue = owner.textField.text ?? ""
})
}
#objc fileprivate func onSet() {
owner.textField.resignFirstResponder()
}
}
}
struct DemoCustomKeyboardInput : View {
#State var email:String = ""
var body: some View {
VStack{
CustomInputTextField(text: $email).border(Color.black)
.padding(.horizontal)
.frame(maxHeight: 32)
Divider()
Text("Entered text: \(email)")
}
}
}
struct DemoCustomKeyboardInput_Previews: PreviewProvider {
static var previews: some View {
DemoCustomKeyboardInput()
}
}
I use this code multi line textfield.
SwiftUI
Swift5
Version 11.3 (11C29)
struct MultiLineTextField: UIViewRepresentable {
#Binding var text: String
let onEditingChanged: (Bool) -> Void
init(text: Binding<String>, onEditingChanged: #escaping (Bool) -> Void = {_ in}) {
self._text = text
self.onEditingChanged = onEditingChanged
}
func makeCoordinator() -> MultiLineTextField.Coordinator {
return MultiLineTextField.Coordinator(parent1: self)
}
func makeUIView(context: UIViewRepresentableContext<MultiLineTextField>) -> UITextView {
let textView = UITextView()
textView.isEditable = true
textView.isUserInteractionEnabled = true
textView.isScrollEnabled = true
textView.font = .systemFont(ofSize: 20)
textView.delegate = context.coordinator
textView.text = self.text
/******* toolbar add **********/
let toolbar = UIToolbar()
toolbar.setItems(
[
UIBarButtonItem(
title: "Done",
style: UIBarButtonItem.Style.done,
target: self,
action: nil
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
textView.inputAccessoryView = toolbar
/******* toolbar add **********/
return textView
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MultiLineTextField>) {
if uiView.text != self.text {
uiView.text = self.text
}
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultiLineTextField
let onEditingChanged: (Bool) -> Void
init(parent1: MultiLineTextField, onEditingChanged: #escaping (Bool) -> Void = {_ in}) {
self.parent = parent1
self.onEditingChanged = onEditingChanged
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
onEditingChanged(true)
}
func textViewDidEndEditing(_ textView: UITextView) {
onEditingChanged(false)
}
}
}
I've solved this problem using 99% pure SwiftUI on iOS 14.
That's my implementation:
import SwiftUI
struct ContentView: View {
#State private var showtextFieldToolbar = false
#State private var text = ""
var body: some View {
ZStack {
VStack {
TextField("Write here", text: $text) { isChanged in
if isChanged {
showtextFieldToolbar = true
}
} onCommit: {
showtextFieldToolbar = false
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
VStack {
Spacer()
if showtextFieldToolbar {
HStack {
Spacer()
Button("Close") {
showtextFieldToolbar = false
UIApplication.shared
.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
.foregroundColor(Color.black)
.padding(.trailing, 12)
}
.frame(idealWidth: .infinity, maxWidth: .infinity,
idealHeight: 44, maxHeight: 44,
alignment: .center)
.background(Color.gray)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct InputAccessory: UIViewRepresentable {
var placeHolder: String
func makeUIView(context: Context) -> UITextField {
let toolbar = UIToolbar()
toolbar.setItems([
// just moves the Done item to the right
UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace
, target: nil
, action: nil
)
, UIBarButtonItem(
title: "Done"
, style: UIBarButtonItem.Style.done
, target: self
, action: nil
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))
customView.backgroundColor = UIColor.red
let sampleTextField = UITextField(frame: CGRect(x: 20, y: 100, width: 300, height: 40))
sampleTextField.inputAccessoryView = toolbar
sampleTextField.placeholder = placeHolder
return sampleTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.endEditing(true)
}
}
struct ContentView : View {
#State var email:String = "e"
var body: some View {
HStack{
Circle()
InputAccessory(placeHolder: "hello")
Circle()
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Now you can hide and show the textfield with the "showInput" state. The next problem is, that you have to open your keyboard at a certain event and show the textfield. That's again not possible with SwiftUI and you have to go back to UiKit and making it first responder.
Overall, at the current state it's not possible to work with the keyboard or with the certain textfield method.

SwiftUI - Custom Swipe Actions In List

How can I use custom Swipe Actions in SwiftUI?
I tried to use the UIKit Framework to get these working in SwiftUI. But that doesn't work for me.
import SwiftUI
import UIKit
init() {
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let important = importantAction(at: indexPath)
return UISwipeActionsConfiguration(actions: [important])
}
func importantAction(at indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Important") { (action, view, completion) in
print("HI")
}
action.backgroundColor = UIColor(hue: 0.0861, saturation: 0.76, brightness: 0.94, alpha: 1.0) /* #f19938 */
action.image = UIImage(named: "pencil")
return action
}
}
struct TestView: View {
NavigationView {
List {
ForEach(appointmentsViewModel.appointments.identified(by: \.id)) { appointment in Row_Appointments(appointment: appointment)
}.onDelete(perform: delete)
}
}
}
}
If your deployment target is iOS 15 (or newer), then you can use the swipeActions modifier to customize the swipe actions of a list item.
This also applies to watchOS 8 and macOS 12.
These operating systems will be released in late 2021.
Prior to the late 2021 version of SwiftUI, there is no support for custom swipe actions for List items.
If you need to target an older version, you would probably be better off implementing a different user interface, like adding a toggle button as a subview of your list item, or adding a context menu to your list item.
iOS 15+
In iOS 15 we can finally use native Swipe Actions:
func swipeActions<T>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, content: () -> T) -> some View where T : View
They can be attached to the ForEach container just like onMove or onDelete:
List {
ForEach(appointmentsViewModel.appointments.identified(by: \.id)) { appointment in
Row_Appointments(appointment: appointment)
}
.swipeActions(edge: .trailing) {
Button {
print("Hi")
} label: {
Label("Important", systemImage: "pencil")
}
}
}
Based on MichaƂ Ziobro answer using Introspect to simplify table view delegate setup.
Note that this will override the table view delegate and might BREAK some of the existing table view behaviours. While things such as header hight can be fixed by adding the method to custom delegate yourself, other might not be fixable.
struct ListSwipeActions: ViewModifier {
#ObservedObject var coordinator = Coordinator()
func body(content: Content) -> some View {
return content
.introspectTableView { tableView in
tableView.delegate = self.coordinator
}
}
class Coordinator: NSObject, ObservableObject, UITableViewDelegate {
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let archiveAction = UIContextualAction(style: .normal, title: "Title") { action, view, completionHandler in
// update data source
completionHandler(true)
}
archiveAction.image = UIImage(systemName: "archivebox")!
archiveAction.backgroundColor = .systemYellow
let configuration = UISwipeActionsConfiguration(actions: [archiveAction])
return configuration
}
}
}
extension List {
func swipeActions() -> some View {
return self.modifier(ListSwipeActions())
}
}
It is able to be done in the way something like this:
List {
ForEach(items) { (item) in
Text("\(item.title)")
}
.onDelete(perform: self.delete)
}.swipeActions()
Then you need to add this swipeActions() modifier
struct ListSwipeActions: ViewModifier {
#ObservedObject var coordinator = Coordinator()
func body(content: Content) -> some View {
return content
.background(TableViewConfigurator(configure: { tableView in
delay {
tableView.delegate = self.coordinator
}
}))
}
class Coordinator: NSObject, ObservableObject, UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("Scrolling ....!!!")
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let isArchived = false
let title = isArchived ? NSLocalizedString("Unarchive", comment: "Unarchive") : NSLocalizedString("Archive", comment: "Archive")
let archiveAction = UIContextualAction(style: .normal, title: title, handler: {
(action, view, completionHandler) in
// update data source
completionHandler(true)
})
archiveAction.title = title
archiveAction.image = UIImage(systemName: "archivebox")!
archiveAction.backgroundColor = .systemYellow
let configuration = UISwipeActionsConfiguration(actions: [archiveAction])
return configuration
}
}
}
extension List {
func swipeActions() -> some View {
return self.modifier(ListSwipeActions())
}
}
And have TableViewConfigurator that searches for table view behind the List
struct TableViewConfigurator: UIViewControllerRepresentable {
var configure: (UITableView) -> Void = { _ in }
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
let tableViews = UIApplication.nonModalTopViewController()?.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()
for tableView in tableViews {
self.configure(tableView)
}
}
}
Delighted to see that iOS 15 brings the long awaited .swipeActions view modifier to List in SwiftUI with an easy to use API.
List {
ForEach(store.messages) { message in
MessageCell(message: message)
.swipeActions(edge: .leading) {
Button { store.toggleUnread(message) } label: {
if message.isUnread {
Label("Read", systemImage: "envelope.open")
} else {
Label("Unread", systemImage: "envelope.badge")
}
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
store.delete(message)
} label: {
Label("Delete", systemImage: "trash")
}
Button { store.flag(message) } label: {
Label("Flag", systemImage: "flag")
}
}
}
}
}
Actions appear in the order listed, starting from the originating edge working inwards.
The example above produces:
Note that swipeActions override the onDelete handler if provided that is available on ForEach
Read more in Apple's developer docs
I wanted the same and have now the following implementation.
The SwipeController checks when to execute a swipe action and performs the SwipeAction, for now you can add your swipe actions under the print lines in the executeAction function. But it is better make an abstract class from this.
Then in the SwipeLeftRightContainer struct we have most of the logic in the DragGesture. What it does is while your dragging its gonna change the offset and then make calls to the SwipeController to see if the threshold for swipe left or right are reached. Then when you finish the dragging it will come into the onEnded callback of the DragGesture. Here we will reset the offset and let the SwipeController decide to execute an action.
Keep in mind lot of the variables in the view are static for an iPhone X so you should change them to what fits best.
Also what this does is creating an action for left and right swipe, but you can adjust it to your own use ofcourse.
import SwiftUI
/** executeRight: checks if it should execute the swipeRight action
execute Left: checks if it should execute the swipeLeft action
submitThreshold: the threshold of the x offset when it should start executing the action
*/
class SwipeController {
var executeRight = false
var executeLeft = false
let submitThreshold: CGFloat = 200
func checkExecutionRight(offsetX: CGFloat) {
if offsetX > submitThreshold && self.executeRight == false {
Utils.HapticSuccess()
self.executeRight = true
} else if offsetX < submitThreshold {
self.executeRight = false
}
}
func checkExecutionLeft(offsetX: CGFloat) {
if offsetX < -submitThreshold && self.executeLeft == false {
Utils.HapticSuccess()
self.executeLeft = true
} else if offsetX > -submitThreshold {
self.executeLeft = false
}
}
func excuteAction() {
if executeRight {
print("executed right")
} else if executeLeft {
print("executed left")
}
self.executeLeft = false
self.executeRight = false
}
}
struct SwipeLeftRightContainer: View {
var swipeController: SwipeController = SwipeController()
#State var offsetX: CGFloat = 0
let maxWidth: CGFloat = 335
let maxHeight: CGFloat = 125
let swipeObjectsOffset: CGFloat = 350
let swipeObjectsWidth: CGFloat = 400
#State var rowAnimationOpacity: Double = 0
var body: some View {
ZStack {
Group {
HStack {
Text("Sample row")
Spacer()
}
}.padding(10)
.zIndex(1.0)
.frame(width: maxWidth, height: maxHeight)
.cornerRadius(5)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.gray))
.padding(10)
.offset(x: offsetX)
.gesture(DragGesture(minimumDistance: 5).onChanged { gesture in
withAnimation(Animation.linear(duration: 0.1)) {
offsetX = gesture.translation.width
}
swipeController.checkExecutionLeft(offsetX: offsetX)
swipeController.checkExecutionRight(offsetX: offsetX)
}.onEnded { _ in
withAnimation(Animation.linear(duration: 0.1)) {
offsetX = 0
swipeController.prevLocX = 0
swipeController.prevLocXDiff = 0
self.swipeController.excuteAction()
}
})
Group {
ZStack {
Rectangle().fill(Color.red).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityDelete)
Image(systemName: "multiply").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.trailing, 150)
}
}.zIndex(0.9).offset(x: swipeObjectsOffset + offsetX)
Group {
ZStack {
Rectangle().fill(Color.green).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityLike)
Image(systemName: "heart").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.leading, 150)
}
}.zIndex(0.9).offset(x: -swipeObjectsOffset + offsetX)
}
}
var opacityDelete: Double {
if offsetX < 0 {
return Double(abs(offsetX) / 50)
}
return 0
}
var opacityLike: Double {
if offsetX > 0 {
return Double(offsetX / 50)
}
return 0
}
}
struct SwipeListView: View {
var body: some View {
ScrollView {
ForEach(0..<10) { index in
SwipeLeftRightContainer().listRowInsets(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
}
struct SwipeLeftRight_Previews: PreviewProvider {
static var previews: some View {
SwipeListView()
}
}
Now with IOS 15 , Swift 5.5 we can add a Swipe action like this
struct ContentView: View {
#State private var total = 0
var body: some View {
NavigationView {
List {
ForEach(1..<100) { i in
Text("\(i)")
.swipeActions(edge: .leading) {
Button {
total += i
} label: {
Label("Add \(i)", systemImage: "plus.circle")
}
.tint(.indigo)
}
.swipeActions(edge: .trailing) {
Button {
total -= i
} label: {
Label("Subtract \(i)", systemImage: "minus.circle")
}
}
}
}
.navigationTitle("Total: \(total)")
}
}
}