Related
I am new to SwiftUI. I have three views and I want them in a PageView. I want to move each Views by swipe like a pageview and I want the little dots to indicate in which view I'm in.
iOS 15+
In iOS 15 a new TabViewStyle was introduced: CarouselTabViewStyle (watchOS only).
Also, we can now set styles more easily:
.tabViewStyle(.page)
iOS 14+
There is now a native equivalent of UIPageViewController in SwiftUI 2 / iOS 14.
To create a paged view, add the .tabViewStyle modifier to TabView and pass PageTabViewStyle.
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TabView {
FirstView()
SecondView()
ThirdView()
}
.tabViewStyle(PageTabViewStyle())
}
}
}
You can also control how the paging dots are displayed:
// hide paging dots
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
You can find a more detailed explanation in this link:
How to create scrolling pages of content using tabViewStyle()
Vertical variant
TabView {
Group {
FirstView()
SecondView()
ThirdView()
}
.rotationEffect(Angle(degrees: -90))
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.rotationEffect(Angle(degrees: 90))
Custom component
If you're tired of passing tabViewStyle every time you can create your own PageView:
Note: TabView selection in iOS 14.0 worked differently and that's why I used two Binding properties: selectionInternal and selectionExternal. As of iOS 14.3 it seems to be working with just one Binding. However, you can still access the original code from the revision history.
struct PageView<SelectionValue, Content>: View where SelectionValue: Hashable, Content: View {
#Binding private var selection: SelectionValue
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private let indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode
private let content: () -> Content
init(
selection: Binding<SelectionValue>,
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
#ViewBuilder content: #escaping () -> Content
) {
self._selection = selection
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
var body: some View {
TabView(selection: $selection) {
content()
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: indexBackgroundDisplayMode))
}
}
extension PageView where SelectionValue == Int {
init(
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
#ViewBuilder content: #escaping () -> Content
) {
self._selection = .constant(0)
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
}
Now you have a default PageView:
PageView {
FirstView()
SecondView()
ThirdView()
}
which can be customised:
PageView(indexDisplayMode: .always, indexBackgroundDisplayMode: .always) { ... }
or provided with a selection:
struct ContentView: View {
#State var selection = 1
var body: some View {
VStack {
Text("Selection: \(selection)")
PageView(selection: $selection, indexBackgroundDisplayMode: .always) {
ForEach(0 ..< 3, id: \.self) {
Text("Page \($0)")
.tag($0)
}
}
}
}
}
Page Control
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
control.pageIndicatorTintColor = UIColor.lightGray
control.currentPageIndicatorTintColor = UIColor.darkGray
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
return control
}
func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}
class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
#objc
func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
}
Your page View
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
ZStack(alignment: .bottom) {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
}
}
}
Your page View Controller
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
#State private var previousPage = 0
init(controllers: [UIViewController],
currentPage: Binding<Int>)
{
self.controllers = controllers
self._currentPage = currentPage
self.previousPage = currentPage.wrappedValue
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
guard !controllers.isEmpty else {
return
}
let direction: UIPageViewController.NavigationDirection = previousPage < currentPage ? .forward : .reverse
context.coordinator.parent = self
pageViewController.setViewControllers(
[controllers[currentPage]], direction: direction, animated: true) { _ in {
previousPage = currentPage
}
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
Let's say you have a view like
struct CardView: View {
var album: Album
var body: some View {
URLImage(URL(string: album.albumArtWork)!)
.resizable()
.aspectRatio(3 / 2, contentMode: .fit)
}
}
You can use this component in your main SwiftUI view like this.
PageView(vM.Albums.map { CardView(album: $0) }).frame(height: 250)
iOS 13+ (private API)
Warning: The following answer uses private SwiftUI methods that aren't publicly visible (you can still access them if you know where to look). However, they are not documented properly and may be unstable. Use them at your own risk.
While browsing SwiftUI files I stumbled upon the _PagingView that seems to be available since iOS 13:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingView<Views> : SwiftUI.View where Views : Swift.RandomAccessCollection, Views.Element : SwiftUI.View, Views.Index : Swift.Hashable
This view has two initialisers:
public init(config: SwiftUI._PagingViewConfig = _PagingViewConfig(), page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
public init(direction: SwiftUI._PagingViewConfig.Direction, page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
What we also have is the _PagingViewConfig:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingViewConfig : Swift.Equatable {
public enum Direction {
case vertical
case horizontal
public static func == (a: SwiftUI._PagingViewConfig.Direction, b: SwiftUI._PagingViewConfig.Direction) -> Swift.Bool
public var hashValue: Swift.Int {
get
}
public func hash(into hasher: inout Swift.Hasher)
}
public var direction: SwiftUI._PagingViewConfig.Direction
public var size: CoreGraphics.CGFloat?
public var margin: CoreGraphics.CGFloat
public var spacing: CoreGraphics.CGFloat
public var constrainedDeceleration: Swift.Bool
public init(direction: SwiftUI._PagingViewConfig.Direction = .horizontal, size: CoreGraphics.CGFloat? = nil, margin: CoreGraphics.CGFloat = 0, spacing: CoreGraphics.CGFloat = 0, constrainedDeceleration: Swift.Bool = true)
public static func == (a: SwiftUI._PagingViewConfig, b: SwiftUI._PagingViewConfig) -> Swift.Bool
}
Now, we can create a simple _PagingView:
_PagingView(direction: .horizontal, views: [
AnyView(Color.red),
AnyView(Text("Hello world")),
AnyView(Rectangle().frame(width: 100, height: 100))
])
Here is another, more customised example:
struct ContentView: View {
#State private var selection = 1
var body: some View {
_PagingView(
config: _PagingViewConfig(
direction: .vertical,
size: nil,
margin: 10,
spacing: 10,
constrainedDeceleration: false
),
page: $selection,
views: [
AnyView(Color.red),
AnyView(Text("Hello world")),
AnyView(Rectangle().frame(width: 100, height: 100))
]
)
}
}
For apps that target iOS 14 and later, the answer suggested by #pawello2222 should be considered the correct one. I have tried it in two apps now and it works great, with very little code.
I have wrapped the proposed concept in a struct that can be provided with both views as well as with an item list and a view builder. It can be found here. The code looks like this:
#available(iOS 14.0, *)
public struct MultiPageView: View {
public init<PageType: View>(
pages: [PageType],
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
currentPageIndex: Binding<Int>) {
self.pages = pages.map { AnyView($0) }
self.indexDisplayMode = indexDisplayMode
self.currentPageIndex = currentPageIndex
}
public init<Model, ViewType: View>(
items: [Model],
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
currentPageIndex: Binding<Int>,
pageBuilder: (Model) -> ViewType) {
self.pages = items.map { AnyView(pageBuilder($0)) }
self.indexDisplayMode = indexDisplayMode
self.currentPageIndex = currentPageIndex
}
private let pages: [AnyView]
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private var currentPageIndex: Binding<Int>
public var body: some View {
TabView(selection: currentPageIndex) {
ForEach(Array(pages.enumerated()), id: \.offset) {
$0.element.tag($0.offset)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
}
}
first you adds the package https://github.com/xmartlabs/PagerTabStripView
then
import SwiftUI
import PagerTabStripView
struct MyPagerView: View {
var body: some View {
PagerTabStripView() {
FirstView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "ACCOUNT", systomIcon: "character.bubble.fill")
}
ContentView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "PROFILE", systomIcon: "person.circle.fill")
}
NewsAPIView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "PASSWORD", systomIcon: "lock.fill")
}
}
.pagerTabStripViewStyle(.barButton(indicatorBarHeight: 4, indicatorBarColor: .black, tabItemSpacing: 0, tabItemHeight: 90))
}
}
struct TitleNavBarItem: View {
let title: String
let systomIcon: String
var body: some View {
VStack {
Image(systemName: systomIcon)
.foregroundColor( .white)
.font(.title)
Text( title)
.font(.system(size: 22))
.bold()
.foregroundColor(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.orange)
}
}
The easiest way to do this is via iPages.
import SwiftUI
import iPages
struct ContentView: View {
#State var currentPage = 0
var body: some View {
iPages(currentPage: $currentPage) {
Text("😋")
Color.pink
}
}
}
So I am trying to create a custom pagination scrollView. I have been able to create that wrapper and the content inside that wrapper consists of a custom View. Inside that custom View i have got two NavigationLink buttons when pressed should take users to two different Views.
Those NavigationLink buttons are not working.
The scrollViewWrapper is inside a NavigationView. I created a test button which is just a simple Button and that seems to work. So there is something that I am not doing correctly with NavigationLink and custom UIViewControllerRepresentable.
This is where I am using the custom wrapper.
NavigationView {
UIScrollViewWrapper {
HStack(spacing: 0) {
ForEach(self.onboardingDataArray, id: \.id) { item in
OnboardingView(onboardingData: item)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.blue)
}
The onboarding view:
struct OnboardingView: View {
var onboardingData: OnboardingModel
var body: some View {
GeometryReader { geometry in
VStack(spacing: 10) {
Spacer()
Image("\(self.onboardingData.image)")
.resizable()
.frame(width: 300, height: 300)
.aspectRatio(contentMode: ContentMode.fit)
.clipShape(Circle())
.padding(20)
Text("\(self.onboardingData.titleText)")
.frame(width: geometry.size.width, height: 20, alignment: .center)
.font(.title)
Text("\(self.onboardingData.descriptionText)")
.lineLimit(nil)
.padding(.leading, 15)
.padding(.trailing, 15)
.font(.system(size: 16))
.frame(width: geometry.size.width, height: 50, alignment: .center)
.multilineTextAlignment(.center)
Spacer(minLength: 20)
if self.onboardingData.showButton ?? false {
VStack {
Button(action: {
print("Test")
}) {
Text("Test Button")
}
NavigationLink(destination: LogInView()) {
Text("Login!")
}
NavigationLink(destination: SignUpView()) {
Text("Sign Up!")
}
}
}
Spacer()
}
}
}
}
The custom ScrollView Wrapper code:
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewController {
let vc = UIScrollViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
class UIScrollViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.isPagingEnabled = true
return view
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
As the other answers have stated, there is an issue with putting the NavigationLink inside the UIViewControllerRepresentable.
I solved this by wrapping my UIViewControllerRepresentable and a NavigationLink inside a View and programmatically activating the NavigationLink from inside the UIViewControllerRepresentable.
For example:
struct MyView: View
{
#State var destination: AnyView? = nil
#State var is_active: Bool = false
var body: some View
{
ZStack
{
MyViewControllerRepresentable( self )
NavigationLink( destination: self.destination, isActive: self.$is_active )
{
EmptyView()
}
}
}
func goTo( destination: AnyView )
{
self.destination = destination
self.is_active = true
}
}
In my case, I passed the MyView instance to the UIViewController that my MyViewControllerRepresentable is wrapping, and called my goTo(destination:AnyView) method when a button was clicked.
The difference between our cases is that my UIViewController was my own class written with UIKit (compared to a UIHostingController). In the case that you're using a UIHostingController, you could probably use a shared ObservableObject containing the destination and is_active variables. You'd change your 2 NavigationLinks to Buttons having the action methods change the ObservableObject's destination and is_active variables.
This happens because you use a UIViewControllerRepresentable instead of UIViewRepresentable. I guess the UIScrollViewController keeps the destination controller from being presented by the current controller.
Try the code above instead:
import UIKit
import SwiftUI
struct ScrollViewWrapper<Content>: UIViewRepresentable where Content: View{
func updateUIView(_ uiView: UIKitScrollView, context: UIViewRepresentableContext<ScrollViewWrapper<Content>>) {
}
typealias UIViewType = UIKitScrollView
let content: () -> Content
var showsIndicators : Bool
public init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.showsIndicators = showsIndicators
}
func makeUIView(context: UIViewRepresentableContext<ScrollViewWrapper>) -> UIViewType {
let hosting = UIHostingController(rootView: AnyView(content()))
let width = UIScreen.main.bounds.width
let size = hosting.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
hosting.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
let view = UIKitScrollView()
view.delegate = view
view.alwaysBounceVertical = true
view.addSubview(hosting.view)
view.contentSize = CGSize(width: width, height: size.height)
return view
}
}
class UIKitScrollView: UIScrollView, UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset) // Do whatever you want.
}
}
This is an extension to the above solution that never scrolls the inner content.
I was into a similar problem. I have figured out that the problem is with the UIViewControllerRepresentable. Instead use UIViewRepresentable, although I am not sure what the issue is. I was able to get the navigationlink work using the below code.
struct SwiftyUIScrollView<Content>: UIViewRepresentable where Content: View {
typealias UIViewType = Scroll
var content: () -> Content
var pagingEnabled: Bool = false
var hideScrollIndicators: Bool = false
#Binding var shouldUpdate: Bool
#Binding var currentIndex: Int
var onScrollIndexChanged: ((_ index: Int) -> Void)
public init(pagingEnabled: Bool,
hideScrollIndicators: Bool,
currentIndex: Binding<Int>,
shouldUpdate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content, onScrollIndexChanged: #escaping ((_ index: Int) -> Void)) {
self.content = content
self.pagingEnabled = pagingEnabled
self._currentIndex = currentIndex
self._shouldUpdate = shouldUpdate
self.hideScrollIndicators = hideScrollIndicators
self.onScrollIndexChanged = onScrollIndexChanged
}
func makeUIView(context: UIViewRepresentableContext<SwiftyUIScrollView>) -> UIViewType {
let hosting = UIHostingController(rootView: content())
let view = Scroll(hideScrollIndicators: hideScrollIndicators, isPagingEnabled: pagingEnabled)
view.scrollDelegate = context.coordinator
view.alwaysBounceHorizontal = true
view.addSubview(hosting.view)
makefullScreen(of: hosting.view, to: view)
return view
}
class Coordinator: NSObject, ScrollViewDelegate {
func didScrollToIndex(_ index: Int) {
self.parent.onScrollIndexChanged(index)
}
var parent: SwiftyUIScrollView
init(_ parent: SwiftyUIScrollView) {
self.parent = parent
}
}
func makeCoordinator() -> SwiftyUIScrollView<Content>.Coordinator {
Coordinator(self)
}
func updateUIView(_ uiView: Scroll, context: UIViewRepresentableContext<SwiftyUIScrollView<Content>>) {
if shouldUpdate {
uiView.scrollToIndex(index: currentIndex)
}
}
func makefullScreen(of childView: UIView, to parentView: UIView) {
childView.translatesAutoresizingMaskIntoConstraints = false
childView.leftAnchor.constraint(equalTo: parentView.leftAnchor).isActive = true
childView.rightAnchor.constraint(equalTo: parentView.rightAnchor).isActive = true
childView.topAnchor.constraint(equalTo: parentView.topAnchor).isActive = true
childView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).isActive = true
}
}
Then create a new class to handle the delegates of a scrollview. You can include the below code into the UIViewRepresentable as well. But I prefer keeping it separated for a clean code.
class Scroll: UIScrollView, UIScrollViewDelegate {
var hideScrollIndicators: Bool = false
var scrollDelegate: ScrollViewDelegate?
var tileWidth = 270
var tileMargin = 20
init(hideScrollIndicators: Bool, isPagingEnabled: Bool) {
super.init(frame: CGRect.zero)
showsVerticalScrollIndicator = !hideScrollIndicators
showsHorizontalScrollIndicator = !hideScrollIndicators
delegate = self
self.isPagingEnabled = isPagingEnabled
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
scrollDelegate?.didScrollToIndex(Int(currentIndex))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
scrollDelegate?.didScrollToIndex(Int(currentIndex))
}
func scrollToIndex(index: Int) {
let newOffSet = CGFloat(tileWidth+tileMargin) * CGFloat(index)
contentOffset = CGPoint(x: newOffSet, y: contentOffset.y)
}
}
Now to implement the scrollView use the below code.
#State private var activePageIndex: Int = 0
#State private var shouldUpdateScroll: Bool = false
SwiftyUIScrollView(pagingEnabled: false, hideScrollIndicators: true, currentIndex: $activePageIndex, shouldUpdate: $shouldUpdateScroll, content: {
HStack(spacing: 20) {
ForEach(self.data, id: \.id) { data in
NavigationLink(destination: self.getTheNextView(data: data)) {
self.cardView(data: data)
}
}
}
.padding(.horizontal, 30.0)
}, onScrollIndexChanged: { (newIndex) in
shouldUpdateScroll = false
activePageIndex = index
// Your own required handling
})
func getTheNextView(data: Any) -> AnyView {
// Return the required destination View
}
Don't forget to add your hosting controller as a child.
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
addChild(self.hostingController)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
Did you set up the Triggered Segues? If you are using Xcode you can right click the button you created in the main storyboard. If it's not set up you can go to the connections inspector on the top right sidebar where you can find the File inspector,Identity inspector, Attributes inspector... and specify the action of what you want your button to do.
The following code gets the keyboard height when the keyboard is displayed and moves the button by the keyboard height.
This movement is performed in the same way at the transition source (ContentView) and the transition destination (SecibdContentView), but the button does not move at the transition destination.
How can I make the buttons move the same on multiple screens?
import SwiftUI
struct ContentView: View {
#ObservedObject private var keyboard = KeyboardResponder()
var body: some View {
NavigationView {
VStack {
Text("ContentView")
Spacer()
NavigationLink(destination: SecondContentView()) {
Text("Next")
}
.offset(x: 0, y: -keyboard.currentHeight)
}
}
}
}
import SwiftUI
struct SecondContentView: View {
#ObservedObject private var keyboard = KeyboardResponder()
var body: some View {
VStack {
Text("SubContentView")
Spacer()
NavigationLink(destination: ThirdContentView()) {
Text("Next")
}
.offset(x: 0, y: -keyboard.currentHeight)
}
}
}
class KeyboardResponder: ObservableObject {
private var _center: NotificationCenter
#Published var currentHeight: CGFloat = 0
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
currentHeight = keyboardSize.height
}
}
#objc func keyBoardWillHide(notification: Notification) {
currentHeight = 0
}
}
Using ViewModifier
You can use ViewModifier of swiftui is much simpler
import SwiftUI
import Combine
struct KeyboardAwareModifier: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
private var keyboardHeightPublisher: AnyPublisher<CGFloat, Never> {
Publishers.Merge(
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue }
.map { $0.cgRectValue.height },
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
).eraseToAnyPublisher()
}
func body(content: Content) -> some View {
content
.padding(.bottom, keyboardHeight)
.onReceive(keyboardHeightPublisher) { self.keyboardHeight = $0 }
}
}
extension View {
func KeyboardAwarePadding() -> some View {
ModifiedContent(content: self, modifier: KeyboardAwareModifier())
}
}
And in your view
struct SomeView: View {
#State private var someText: String = ""
var body: some View {
VStack {
Spacer()
TextField("some text", text: $someText)
}.KeyboardAwarePadding()
}
}
KeyboardAwarePadding() will automatically add a padding in your view, It's more elegant.
SwiftUI + Combine
#Published var keyboardHeight: CGFloat = 0 // if one is in ViewModel: ObservableObject
private var cancellableSet: Set<AnyCancellable> = []
init() {
NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification)
.map {
guard
let info = $0.userInfo,
let keyboardFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
else { return 0 }
return keyboardFrame.height
}
.assign(to: \.keyboardHeight, on: self)
.store(in: &cancellableSet)
NotificationCenter.default.publisher(for: UIWindow.keyboardDidHideNotification)
.map { _ in 0 }
.assign(to: \.keyboardHeight, on: self)
.store(in: &cancellableSet)
}
Your code has a couple of things missing. There's no NavigationView, and no TextFields to make the keyboard appear. Consider updating your code.
Anyway, the problem can be solved easily by replacing: keyboardFrameBeginUserInfoKey with keyboardFrameEndUserInfoKey.
Update:
You also need to use the same KeyboardResponder, otherwise you will creating multiple instances. Alternatively, you can put it into your Evironment.
And you forgot to include TextFields to your code, so the keyboard will show up to test it.
The following code works:
import SwiftUI
struct ContentView: View {
#ObservedObject private var keyboard = KeyboardResponder()
var body: some View {
NavigationView {
VStack {
Text("ContentView")
TextField("enter text", text: .constant(""))
Spacer()
NavigationLink(destination: SecondContentView(keyboard: keyboard)) {
Text("Next")
}
.offset(x: 0, y: -keyboard.currentHeight)
}
}
}
}
import SwiftUI
struct SecondContentView: View {
#ObservedObject var keyboard: KeyboardResponder
var body: some View {
VStack {
Text("SubContentView")
TextField("enter text", text: .constant(""))
Spacer()
NavigationLink(destination: Text("ThirdContentView()")) {
Text("Next")
}
.offset(x: 0, y: -keyboard.currentHeight)
}
}
}
class KeyboardResponder: ObservableObject {
private var _center: NotificationCenter
#Published var currentHeight: CGFloat = 0
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
currentHeight = keyboardSize.height
}
}
#objc func keyBoardWillHide(notification: Notification) {
currentHeight = 0
}
}
So when I make a list in SwiftUI, I get the master-detail split view for "free".
So for instance with this:
import SwiftUI
struct ContentView : View {
var people = ["Angela", "Juan", "Yeji"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
NavigationLink(destination: Text("Hello!")) {
Text(person)
}
}
}
Text("🤪")
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I get a splitView if an iPad simulator is in landscape, and the first detail screen is the emoji. But if people tap on a name, the detail view is "Hello!"
All that is great.
However, if I run the iPad in portrait, the user is greeted by the emoji, and then there is no indication that there is a list. You have to swipe from left to right to make the list appear from the side.
Does anyone know of a way to get even a navigation bar to appear that would let the user tap to see the list of items on the left? So that it's not a screen with the emoji only?
I would hate to leave a note that says "Swipe in from the left to see the list of files/people/whatever"
I remember UISplitViewController had a collapsed property that could be set. Is there anything like that here?
In Xcode 11 beta 3, Apple has added .navigationViewStyle(style:) to NavigationView.
Updated for Xcode 11 Beta 5.
create MasterView() & DetailsView().
struct MyMasterView: View {
var people = ["Angela", "Juan", "Yeji"]
var body: some View {
List {
ForEach(people, id: \.self) { person in
NavigationLink(destination: DetailsView()) {
Text(person)
}
}
}
}
}
struct DetailsView: View {
var body: some View {
Text("Hello world")
.font(.largeTitle)
}
}
inside my ContentView :
var body: some View {
NavigationView {
MyMasterView()
DetailsView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
.padding()
}
Output:
For now, in Xcode 11.2.1 it is still nothing changed.
I had the same issue with SplitView on iPad and resolved it by adding padding like in Ketan Odedra response, but modified it a little:
var body: some View {
GeometryReader { geometry in
NavigationView {
MasterView()
DetailsView()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.padding(.leading, leadingPadding(geometry))
}
}
private func leadingPadding(_ geometry: GeometryProxy) -> CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.5
}
return 0
}
This works perfectly in the simulator. But when I submit my app for review, it was rejected. This little hack doesn't work on the reviewer device. I don't have a real iPad, so I don't know what caused this. Try it, maybe it will work for you.
While it doesn't work for me, I requested help from Apple DTS.
They respond to me that for now, SwiftUI API can't fully simulate UIKit`s SplitViewController behavior. But there is a workaround.
You can create custom SplitView in SwiftUI:
struct SplitView<Master: View, Detail: View>: View {
var master: Master
var detail: Detail
init(#ViewBuilder master: () -> Master, #ViewBuilder detail: () -> Detail) {
self.master = master()
self.detail = detail()
}
var body: some View {
let viewControllers = [UIHostingController(rootView: master), UIHostingController(rootView: detail)]
return SplitViewController(viewControllers: viewControllers)
}
}
struct SplitViewController: UIViewControllerRepresentable {
var viewControllers: [UIViewController]
#Environment(\.splitViewPreferredDisplayMode) var preferredDisplayMode: UISplitViewController.DisplayMode
func makeUIViewController(context: Context) -> UISplitViewController {
return UISplitViewController()
}
func updateUIViewController(_ splitController: UISplitViewController, context: Context) {
splitController.preferredDisplayMode = preferredDisplayMode
splitController.viewControllers = viewControllers
}
}
struct PreferredDisplayModeKey : EnvironmentKey {
static var defaultValue: UISplitViewController.DisplayMode = .automatic
}
extension EnvironmentValues {
var splitViewPreferredDisplayMode: UISplitViewController.DisplayMode {
get { self[PreferredDisplayModeKey.self] }
set { self[PreferredDisplayModeKey.self] = newValue }
}
}
extension View {
/// Sets the preferred display mode for SplitView within the environment of self.
func splitViewPreferredDisplayMode(_ mode: UISplitViewController.DisplayMode) -> some View {
self.environment(\.splitViewPreferredDisplayMode, mode)
}
}
And then use it:
SplitView(master: {
MasterView()
}, detail: {
DetailView()
}).splitViewPreferredDisplayMode(.allVisible)
On an iPad, it works. But there is one issue (maybe more..).
This approach ruins navigation on iPhone because both MasterView and DetailView have their NavigationView.
UPDATE:
Finally, in Xcode 11.4 beta 2 they added a button in Navigation Bar that indicates hidden master view.
Minimal testing in the Simulator, but this should be close to a real solution. The idea is to use an EnvironmentObject to hold a published var on whether to use a double column NavigationStyle, or a single one, then have the NavigationView get recreated if that var changes.
The EnvironmentObject:
final class AppEnvironment: ObservableObject {
#Published var useSideBySide: Bool = false
}
In the Scene Delegate, set the variable at launch, then observe device rotations and possibly change it (the "1000" is not the correct value, starting point):
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var appEnvironment = AppEnvironment()
#objc
func orientationChanged() {
let bounds = UIScreen.main.nativeBounds
let orientation = UIDevice.current.orientation
// 1000 is a starting point, should be smallest height of a + size iPhone
if orientation.isLandscape && bounds.size.height > 1000 {
if appEnvironment.useSideBySide == false {
appEnvironment.useSideBySide = true
print("SIDE changed to TRUE")
}
} else if orientation.isPortrait && appEnvironment.useSideBySide == true {
print("SIDE changed to false")
appEnvironment.useSideBySide = false
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(appEnvironment))
self.window = window
window.makeKeyAndVisible()
orientationChanged()
NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}
In the top level content view, where the NavigationView is created, use a custom modifier instead of using a navigationViewStyle directly:
struct ContentView: View {
#State private var dates = [Date]()
var body: some View {
NavigationView {
MV(dates: $dates)
DetailView()
}
.modifier( WTF() )
}
struct WTF: ViewModifier {
#EnvironmentObject var appEnvironment: AppEnvironment
func body(content: Content) -> some View {
Group {
if appEnvironment.useSideBySide == true {
content
.navigationViewStyle(DoubleColumnNavigationViewStyle())
} else {
content
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
}
}
As mentioned earlier, just Simulator testing, but I tried launching in both orientations, rotating with Master showing, rotating with Detail showing, it all looks good to me.
For current version (iOS 13.0-13.3.x), you can use my code.
I use a UIViewUpdater to access the underlaying UIView and its UIViewController to adjust the bar item.
I think the UIViewUpdater way to solve this problem is the most Swifty and robust way, and you can use it to access and modify other UIView, UIViewController related UIKit mechanism.
ContentView.swift
import SwiftUI
struct ContentView : View {
var people = ["Angela", "Juan", "Yeji"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
NavigationLink(destination: DetailView()) { Text(person) }
}
}
InitialDetailView()
}
}
}
struct DetailView : View {
var body: some View {
Text("Hello!")
}
}
struct InitialDetailView : View {
var body: some View {
NavigationView {
Text("🤪")
.navigationBarTitle("", displayMode: .inline) // .inline is neccesary for showing the left button item
.updateUIViewController {
$0.splitViewController?.preferredDisplayMode = .primaryOverlay // for showing overlay at initial
$0.splitViewController?.preferredDisplayMode = .automatic
}
.displayModeButtonItem()
}.navigationViewStyle(StackNavigationViewStyle())
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Utility code for the solution. Put it in any Swift file for the project.
Utility.swift
// View decoration
public extension View {
func updateUIView(_ action: #escaping (UIView) -> Void) -> some View {
background(UIViewUpdater(action: action).opacity(0))
}
func updateUIViewController(_ action: #escaping (UIViewController) -> Void) -> some View {
updateUIView {
guard let viewController = $0.viewController else { return }
action(viewController)
}
}
func displayModeButtonItem(_ position: NavigationBarPostion = .left) -> some View {
updateUIViewController { $0.setDisplayModeButtonItem(position) }
}
}
// UpdateUIView
struct UIViewUpdater : UIViewRepresentable {
let action: (UIView) -> Void
typealias UIViewType = InnerUIView
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
UIViewType(action: action)
}
func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
// DispatchQueue.main.async { [action] in action(uiView) }
}
class InnerUIView : UIView {
let action: (UIView) -> Void
init(action: #escaping (UIView) -> Void) {
self.action = action
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
super.didMoveToWindow()
update()
}
func update() {
action(self)
}
}
}
// UIView.viewController
public extension UIView {
var viewController: UIViewController? {
var i: UIResponder? = self
while i != nil {
if let vc = i as? UIViewController { return vc }
i = i?.next
}
return nil
}
}
// UIViewController.setDisplayModeButtonItem
public enum NavigationBarPostion {
case left
case right
}
public extension UIViewController {
func setDisplayModeButtonItem(_ position: NavigationBarPostion) {
guard let splitViewController = splitViewController else { return }
switch position {
case .left:
// keep safe to avoid replacing other left bar button item, e.g. navigation back
navigationItem.leftItemsSupplementBackButton = true
navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
case .right:
navigationItem.rightBarButtonItem = splitViewController.displayModeButtonItem
}
}
}
import SwiftUI
var hostingController: UIViewController?
func showList() {
let split = hostingController?.children[0] as? UISplitViewController
UIView.animate(withDuration: 0.3, animations: {
split?.preferredDisplayMode = .primaryOverlay
}) { _ in
split?.preferredDisplayMode = .automatic
}
}
func hideList() {
let split = hostingController?.children[0] as? UISplitViewController
split?.preferredDisplayMode = .primaryHidden
}
// =====
struct Dest: View {
var person: String
var body: some View {
VStack {
Text("Hello! \(person)")
Button(action: showList) {
Image(systemName: "sidebar.left")
}
}
.onAppear(perform: hideList)
}
}
struct ContentView : View {
var people = ["Angela", "Juan", "Yeji"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
NavigationLink(destination: Dest(person: person)) {
Text(person)
}
}
}
VStack {
Text("🤪")
Button(action: showList) {
Image(systemName: "sidebar.left")
}
}
}
}
}
import PlaygroundSupport
hostingController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.setLiveView(hostingController!)
How to hide keyboard using SwiftUI for below cases?
Case 1
I have TextField and I need to hide the keyboard when the user clicks the return button.
Case 2
I have TextField and I need to hide the keyboard when the user taps outside.
How I can do this using SwiftUI?
Note:
I have not asked a question regarding UITextField. I want to do it by using SwifUI.TextField.
You can force the first responder to resign by sending an action to the shared application:
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Now you can use this method to close the keyboard whenever you desire:
struct ContentView : View {
#State private var name: String = ""
var body: some View {
VStack {
Text("Hello \(name)")
TextField("Name...", text: self.$name) {
// Called when the user tap the return button
// see `onCommit` on TextField initializer.
UIApplication.shared.endEditing()
}
}
}
}
If you want to close the keyboard with a tap out, you can create a full screen white view with a tap action, that will trigger the endEditing(_:):
struct Background<Content: View>: View {
private var content: Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
Color.white
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.overlay(content)
}
}
struct ContentView : View {
#State private var name: String = ""
var body: some View {
Background {
VStack {
Text("Hello \(self.name)")
TextField("Name...", text: self.$name) {
self.endEditing()
}
}
}.onTapGesture {
self.endEditing()
}
}
private func endEditing() {
UIApplication.shared.endEditing()
}
}
iOS 15+
(Done button above the keyboard)
Starting with iOS 15 we can now use #FocusState to control which field should be focused (see this answer to see more examples).
We can also add ToolbarItems directly above the keyboard.
When combined together, we can add a Done button right above the keyboard. Here is a simple demo:
struct ContentView: View {
private enum Field: Int, CaseIterable {
case username, password
}
#State private var username: String = ""
#State private var password: String = ""
#FocusState private var focusedField: Field?
var body: some View {
NavigationView {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
}
}
iOS 14+
(Tap anywhere to hide the keyboard)
Here is an updated solution for SwiftUI 2 / iOS 14 (originally proposed here by Mikhail).
It doesn't use the AppDelegate nor the SceneDelegate which are missing if you use the SwiftUI lifecycle:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
}
}
}
extension UIApplication {
func addTapGestureRecognizer() {
guard let window = windows.first else { return }
let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self
window.addGestureRecognizer(tapGesture)
}
}
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true // set to `false` if you don't want to detect tap during other gestures
}
}
If you want to detect other gestures (not only tap gestures) you can use AnyGestureRecognizer as in Mikhail's answer:
let tapGesture = AnyGestureRecognizer(target: window, action: #selector(UIView.endEditing))
Here is an example how to detect simultaneous gestures except Long Press gestures:
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self)
}
}
After a lot of attempts I found a solution that (currently) doesn't block any controls - adding gesture recognizer to UIWindow.
If you want to close keyboard only on Tap outside (without handling drags) - then it's enough to use just UITapGestureRecognizer and just copy step 3:
Create custom gesture recognizer class that works with any touches:
class AnyGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touchedView = touches.first?.view, touchedView is UIControl {
state = .cancelled
} else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable {
state = .cancelled
} else {
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}
In SceneDelegate.swift in the func scene, add next code:
let tapGesture = AnyGestureRecognizer(target: window, action:#selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self //I don't use window as delegate to minimize possible side effects
window?.addGestureRecognizer(tapGesture)
Implement UIGestureRecognizerDelegate to allow simultaneous touches.
extension SceneDelegate: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Now any keyboard on any view will be closed on touch or drag outside.
P.S. If you want to close only specific TextFields - then add and remove gesture recognizer to the window whenever called callback of TextField onEditingChanged
I experienced this while using a TextField inside a NavigationView.
This is my solution for that. It will dismiss the keyboard when you start scrolling.
NavigationView {
Form {
Section {
TextField("Receipt amount", text: $receiptAmount)
.keyboardType(.decimalPad)
}
}
}
.gesture(DragGesture().onChanged{_ in UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)})
#RyanTCB's answer is good; here are a couple of refinements that make it simpler to use and avoid a potential crash:
struct DismissingKeyboard: ViewModifier {
func body(content: Content) -> some View {
content
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
}
The 'bug fix' is simply that keyWindow!.endEditing(true) properly should be keyWindow?.endEditing(true) (yes, you might argue it can't happen.)
More interesting is how you can use it. For example, suppose you have a form with multiple editable fields in it. Just wrap it like this:
Form {
.
.
.
}
.modifier(DismissingKeyboard())
Now, tapping on any control that itself doesn't present a keyboard will do the appropriate dismiss.
(Tested with beta 7)
I found another way to dismiss the keyboard that doesn't require accessing the keyWindow property; as a matter of fact the compiler gives back a warning using
UIApplication.shared.keyWindow?.endEditing(true)
'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes
Instead I used this code:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
Pure SwiftUI (iOS 15)
SwiftUI in iOS 15 (Xcode 13) gained native support for programmatic focus of TextField using new #FocusState property wrapper.
To dismiss the keyboard, simply set view's focusedField to nil. The return key will dismiss keyboard automatically (since iOS 14).
Docs: https://developer.apple.com/documentation/swiftui/focusstate/
struct MyView: View {
enum Field: Hashable {
case myField
}
#State private var text: String = ""
#FocusState private var focusedField: Field?
var body: some View {
TextField("Type here", text: $text)
.focused($focusedField, equals: .myField)
Button("Dismiss") {
focusedField = nil
}
}
}
Pure SwiftUI (iOS 14 and below)
You can completely avoid interaction with UIKit and implement it in pure SwiftUI. Just add an .id(<your id>) modifier to your TextField and change its value whenever you want to dismiss keyboard (on swipe, view tap, button action, ..).
Sample implementation:
struct MyView: View {
#State private var text: String = ""
#State private var textFieldId: String = UUID().uuidString
var body: some View {
VStack {
TextField("Type here", text: $text)
.id(textFieldId)
Spacer()
Button("Dismiss", action: { textFieldId = UUID().uuidString })
}
}
}
Note that I only tested it in latest Xcode 12 beta, but it should work with older versions (even Xcode 11) without any issue.
SwiftUI
in 'SceneDelegate.swift' file just add: .onTapGesture { window.endEditing(true)}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: contentView.onTapGesture { window.endEditing(true)}
)
self.window = window
window.makeKeyAndVisible()
}
}
this is enough for each View using keyboard in your app...
My solution how to hide software keyboard when users tap outside.
You need to use contentShape with onLongPressGesture to detect the entire View container. onTapGesture required to avoid blocking focus on TextField. You can use onTapGesture instead of onLongPressGesture but NavigationBar items won't work.
extension View {
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct KeyboardAvoiderDemo: View {
#State var text = ""
var body: some View {
VStack {
TextField("Demo", text: self.$text)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture {}
.onLongPressGesture(
pressing: { isPressed in if isPressed { self.endEditing() } },
perform: {})
}
}
Since iOS 15, you can use #FocusState
struct ContentView: View {
#Binding var text: String
private enum Field: Int {
case yourTextEdit
}
#FocusState private var focusedField: Field?
var body: some View {
VStack {
TextEditor(text: $speech.text.bound)
.padding(Edge.Set.horizontal, 18)
.focused($focusedField, equals: .yourTextEdit)
}.onTapGesture {
if (focusedField != nil) {
focusedField = nil
}
}
}
}
In iOS15 this is working flawlessly.
VStack {
// Some content
}
.onTapGesture {
// Hide Keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ gesture in
// Hide keyboard on swipe down
if gesture.translation.height > 0 {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}))
Nothing else is required on your TextField and both swipe down along with tap will work to hide it. The way I use this is that on my master NavigationView I add this code and then everything below it will work. The only exception would be that any Sheet would need to have this appended to it as that is acting on a different state.
I prefer using the .onLongPressGesture(minimumDuration: 0), which does not cause the keyboard to blink when another TextView is activated (side effect of .onTapGesture). The hide keyboard code can be a reusable function.
.onTapGesture(count: 2){} // UI is unresponsive without this line. Why?
.onLongPressGesture(minimumDuration: 0, maximumDistance: 0, pressing: nil, perform: hide_keyboard)
func hide_keyboard()
{
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
add this modifier to the view you want to detect user taps
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow!.endEditing(true)
}
Because keyWindow is deprecated.
extension View {
func endEditing(_ force: Bool) {
UIApplication.shared.windows.forEach { $0.endEditing(force)}
}
}
Expanding the answer by josefdolezal above, you can hide keyboard when user taps anywhere outside the textfield like below:
struct SwiftUIView: View {
#State private var textFieldId: String = UUID().uuidString // To hidekeyboard when tapped outside textFields
#State var fieldValue = ""
var body: some View {
VStack {
TextField("placeholder", text: $fieldValue)
.id(textFieldId)
.onTapGesture {} // So that outer tap gesture has no effect on field
// any more views
}
.onTapGesture { // whenever tapped within VStack
textFieldId = UUID().uuidString
//^ this will remake the textfields hence loosing keyboard focus!
}
}
}
Keyboard's Return Key
In addition to all answers about tapping outside of the textField, you may want to dismiss the keyboard when the user taps the return key on the keyboard:
define this global function:
func resignFirstResponder() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
And add use in onCommit argument it:
TextField("title", text: $text, onCommit: {
resignFirstResponder()
})
Benefits
You can call it from anywhere
It's not dependent on UIKit or SwiftUI (can be used in mac apps)
It works even in iOS 13
Demo
Updated the answer, working with Swift 5.7:
extension UIApplication {
func dismissKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
and then using it where needed like for example as button action:
Button(action: {
// do stuff
UIApplication.shared.dismissKeyboard()
}, label: { Text("MyButton") })
Expanding on the answer by #Feldur (which was based on #RyanTCB's), here is an even more expressive and powerful solution allowing you to dismiss keyboard on other gestures than onTapGesture, you can specify which you want in the function call.
Usage
// MARK: - View
extension RestoreAccountInputMnemonicScreen: View {
var body: some View {
List(viewModel.inputWords) { inputMnemonicWord in
InputMnemonicCell(mnemonicInput: inputMnemonicWord)
}
.dismissKeyboard(on: [.tap, .drag])
}
}
Or using All.gestures (just sugar for Gestures.allCases 🍬)
.dismissKeyboard(on: All.gestures)
Code
enum All {
static let gestures = all(of: Gestures.self)
private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
return CI.allCases
}
}
enum Gestures: Hashable, CaseIterable {
case tap, longPress, drag, magnification, rotation
}
protocol ValueGesture: Gesture where Value: Equatable {
func onChanged(_ action: #escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}
extension Gestures {
#discardableResult
func apply<V>(to view: V, perform voidAction: #escaping () -> Void) -> AnyView where V: View {
func highPrio<G>(
gesture: G
) -> AnyView where G: ValueGesture {
view.highPriorityGesture(
gesture.onChanged { value in
_ = value
voidAction()
}
).eraseToAny()
}
switch self {
case .tap:
// not `highPriorityGesture` since tapping is a common gesture, e.g. wanna allow users
// to easily tap on a TextField in another cell in the case of a list of TextFields / Form
return view.gesture(TapGesture().onEnded(voidAction)).eraseToAny()
case .longPress: return highPrio(gesture: LongPressGesture())
case .drag: return highPrio(gesture: DragGesture())
case .magnification: return highPrio(gesture: MagnificationGesture())
case .rotation: return highPrio(gesture: RotationGesture())
}
}
}
struct DismissingKeyboard: ViewModifier {
var gestures: [Gestures] = Gestures.allCases
dynamic func body(content: Content) -> some View {
let action = {
let forcing = true
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(forcing)
}
return gestures.reduce(content.eraseToAny()) { $1.apply(to: $0, perform: action) }
}
}
extension View {
dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
}
}
Word of caution
Please do note that if you use all gestures they might conflict and I did not come up with any neat solution solving that.
Please check https://github.com/michaelhenry/KeyboardAvoider
Just include KeyboardAvoider {} on top of your main view and that's all.
KeyboardAvoider {
VStack {
TextField()
TextField()
TextField()
TextField()
}
}
Well, the easiest solution for me is to simply use the library here.
SwiftUI support is somewhat limited, I use it by placing this code in the #main struct:
import IQKeyboardManagerSwift
#main
struct MyApp: App {
init(){
IQKeyboardManager.shared.enable = true
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
}
...
}
This method allows you to hide the keyboard on spacers!
First add this function (Credit Given To: Casper Zandbergen, from SwiftUI can't tap in Spacer of HStack)
extension Spacer {
public func onTapGesture(count: Int = 1, perform action: #escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}
Next add the following 2 functions (Credit Given To: rraphael, from this question)
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
The function below would be added to your View class, just refer to the top answer here from rraphael for more details.
private func endEditing() {
UIApplication.shared.endEditing()
}
Finally, you can now simply call...
Spacer().onTapGesture {
self.endEditing()
}
This will make any spacer area close the keyboard now. No need for a big white background view anymore!
You could hypothetically apply this technique of extension to any controls you need to support TapGestures that do not currently do so and call the onTapGesture function in combination with self.endEditing() to close the keyboard in any situation you desire.
Based on #Sajjon's answer, here is a solution allowing you to dismiss keyboard on tap, long press, drag, magnification and rotation gestures according to your choice.
This solution is working in XCode 11.4
Usage to get the behavior asked by #IMHiteshSurani
struct MyView: View {
#State var myText = ""
var body: some View {
VStack {
DismissingKeyboardSpacer()
HStack {
TextField("My Text", text: $myText)
Button("Return", action: {})
.dismissKeyboard(on: [.longPress])
}
DismissingKeyboardSpacer()
}
}
}
struct DismissingKeyboardSpacer: View {
var body: some View {
ZStack {
Color.black.opacity(0.0001)
Spacer()
}
.dismissKeyboard(on: Gestures.allCases)
}
}
Code
enum All {
static let gestures = all(of: Gestures.self)
private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
return CI.allCases
}
}
enum Gestures: Hashable, CaseIterable {
case tap, longPress, drag, magnification, rotation
}
protocol ValueGesture: Gesture where Value: Equatable {
func onChanged(_ action: #escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}
extension Gestures {
#discardableResult
func apply<V>(to view: V, perform voidAction: #escaping () -> Void) -> AnyView where V: View {
func highPrio<G>(gesture: G) -> AnyView where G: ValueGesture {
AnyView(view.highPriorityGesture(
gesture.onChanged { _ in
voidAction()
}
))
}
switch self {
case .tap:
return AnyView(view.gesture(TapGesture().onEnded(voidAction)))
case .longPress:
return highPrio(gesture: LongPressGesture())
case .drag:
return highPrio(gesture: DragGesture())
case .magnification:
return highPrio(gesture: MagnificationGesture())
case .rotation:
return highPrio(gesture: RotationGesture())
}
}
}
struct DismissingKeyboard: ViewModifier {
var gestures: [Gestures] = Gestures.allCases
dynamic func body(content: Content) -> some View {
let action = {
let forcing = true
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(forcing)
}
return gestures.reduce(AnyView(content)) { $1.apply(to: $0, perform: action) }
}
}
extension View {
dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
}
}
Something I found that works very nice is
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Then add to the view struct:
private func endEditing() {
UIApplication.shared.endEditing()
}
Then
struct YourView: View {
var body: some View {
ParentView {
//...
}.contentShape(Rectangle()) //<---- This is key!
.onTapGesture {endEditing()}
}
}
So far above options did not work for me, because I have Form and inside buttons, links, picker ...
I create below code that is working, with help from above examples.
import Combine
import SwiftUI
private class KeyboardListener: ObservableObject {
#Published var keyabordIsShowing: Bool = false
var cancellable = Set<AnyCancellable>()
init() {
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.sink { [weak self ] _ in
self?.keyabordIsShowing = true
}
.store(in: &cancellable)
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.sink { [weak self ] _ in
self?.keyabordIsShowing = false
}
.store(in: &cancellable)
}
}
private struct DismissingKeyboard: ViewModifier {
#ObservedObject var keyboardListener = KeyboardListener()
fileprivate func body(content: Content) -> some View {
ZStack {
content
Rectangle()
.background(Color.clear)
.opacity(keyboardListener.keyabordIsShowing ? 0.01 : 0)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({ $0.activationState == .foregroundActive })
.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first?.windows
.filter({ $0.isKeyWindow }).first
keyWindow?.endEditing(true)
}
}
}
}
extension View {
func dismissingKeyboard() -> some View {
ModifiedContent(content: self, modifier: DismissingKeyboard())
}
}
Usage:
var body: some View {
NavigationView {
Form {
picker
button
textfield
text
}
.dismissingKeyboard()
Simple solution for clicking "outside" that worked for me:
First provide a ZStack before all views. In it, put a background (with the color of your choosing) and supply a tap Gesture. In the gesture call, invoke the 'sendAction' we've seen above:
import SwiftUI
struct MyView: View {
private var myBackgroundColor = Color.red
#State var text = "text..."
var body: some View {
ZStack {
self.myBackgroundColor.edgesIgnoringSafeArea(.all)
.onTapGesture(count: 1) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
TextField("", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
A cleaner SwiftUI-native way to dismiss the keyboard via tap without blocking any complicated forms or whatnot... credit to #user3441734 for flagging GestureMask as a clean approach.
Monitor UIWindow.keyboardWillShowNotification / willHide
Pass the current keyboard state via an EnvironmentKey set at the/a root view
Tested for iOS 14.5.
Attach dismiss gesture to the form
Form { }
.dismissKeyboardOnTap()
Setup monitor in root view
// Root view
.environment(\.keyboardIsShown, keyboardIsShown)
.onDisappear { dismantleKeyboarMonitors() }
.onAppear { setupKeyboardMonitors() }
// Monitors
#State private var keyboardIsShown = false
#State private var keyboardHideMonitor: AnyCancellable? = nil
#State private var keyboardShownMonitor: AnyCancellable? = nil
func setupKeyboardMonitors() {
keyboardShownMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillShowNotification)
.sink { _ in if !keyboardIsShown { keyboardIsShown = true } }
keyboardHideMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillHideNotification)
.sink { _ in if keyboardIsShown { keyboardIsShown = false } }
}
func dismantleKeyboarMonitors() {
keyboardHideMonitor?.cancel()
keyboardShownMonitor?.cancel()
}
SwiftUI Gesture + Sugar
struct HideKeyboardGestureModifier: ViewModifier {
#Environment(\.keyboardIsShown) var keyboardIsShown
func body(content: Content) -> some View {
content
.gesture(TapGesture().onEnded {
UIApplication.shared.resignCurrentResponder()
}, including: keyboardIsShown ? .all : .none)
}
}
extension UIApplication {
func resignCurrentResponder() {
sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
extension View {
/// Assigns a tap gesture that dismisses the first responder only when the keyboard is visible to the KeyboardIsShown EnvironmentKey
func dismissKeyboardOnTap() -> some View {
modifier(HideKeyboardGestureModifier())
}
/// Shortcut to close in a function call
func resignCurrentResponder() {
UIApplication.shared.resignCurrentResponder()
}
}
EnvironmentKey
extension EnvironmentValues {
var keyboardIsShown: Bool {
get { return self[KeyboardIsShownEVK] }
set { self[KeyboardIsShownEVK] = newValue }
}
}
private struct KeyboardIsShownEVK: EnvironmentKey {
static let defaultValue: Bool = false
}
True SwiftUI Solution
#State var dismissKeyboardToggle = false
var body: some View {
if dismissKeyboardToggle {
textfield
} else {
textfield
}
Button("Hide Keyboard") {
dismissKeyboardToggle.toggle()
}
}
this will work flawlessly
I am trying to hide keyboard while single tap & Picker should also work with single tap in SwiftUIForms.
I searched a lot to find a proper solution but didn't get any which works for me. So I make my own extension which works very well.
Use in your SwiftUI Form View:
var body: some View {
.onAppear { KeyboardManager.shared.setCurrentView(UIApplication.topViewController()?.view)
}
}
KeyboardManager Utility:
enum KeyboardNotificationType {
case show
case hide
}
typealias KeyBoardSizeBlock = ((CGSize?, UIView?, KeyboardNotificationType) -> Void)
class KeyboardManager: NSObject {
static let shared = KeyboardManager()
private weak var view: UIView?
var didReceiveKeyboardEvent: KeyBoardSizeBlock?
#objc public var shouldResignOnTouchOutside = true {
didSet {
resignFirstResponderGesture.isEnabled = shouldResignOnTouchOutside
}
}
#objc lazy public var resignFirstResponderGesture: UITapGestureRecognizer = {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissCurrentKeyboard))
tap.cancelsTouchesInView = false
tap.delegate = self
return tap
}()
private override init() {
super.init()
self.setup()
}
func setCurrentView(_ view: UIView?) {
self.view = view
resignFirstResponderGesture.isEnabled = true
if let view = self.view {
view.addGestureRecognizer(resignFirstResponderGesture)
}
}
private func setup() {
registerForKeyboardWillShowNotification()
registerForKeyboardWillHideNotification()
}
private func topViewHasCurrenView() -> Bool {
if view == nil { return false }
let currentView = UIApplication.topViewController()?.view
if currentView == view { return true }
for subview in UIApplication.topViewController()?.view.subviews ?? [] where subview == view {
return true
}
return false
}
#objc func dismissCurrentKeyboard() {
view?.endEditing(true)
}
func removeKeyboardObserver(_ observer: Any) {
NotificationCenter.default.removeObserver(observer)
}
private func findFirstResponderInViewHierarchy(_ view: UIView) -> UIView? {
for subView in view.subviews {
if subView.isFirstResponder {
return subView
} else {
let result = findFirstResponderInViewHierarchy(subView)
if result != nil {
return result
}
}
}
return nil
}
deinit {
removeKeyboardObserver(self)
}
}
// MARK: - Keyboard Notifications
extension KeyboardManager {
private func registerForKeyboardWillShowNotification() {
_ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
guard let `self` = self else { return }
guard let userInfo = notification.userInfo else { return }
guard var kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
kbRect.size.height -= self.view?.safeAreaInsets.bottom ?? 0.0
var mainResponder: UIView?
guard self.topViewHasCurrenView() else { return }
if let scrollView = self.view as? UIScrollView {
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: kbRect.size.height, right: 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
guard let firstResponder = self.findFirstResponderInViewHierarchy(scrollView) else {
return
}
mainResponder = firstResponder
var aRect = scrollView.frame
aRect.size.height -= kbRect.size.height
if (!aRect.contains(firstResponder.frame.origin) ) {
scrollView.scrollRectToVisible(firstResponder.frame, animated: true)
}
} else if let tableView = self.view as? UITableView {
guard let firstResponder = self.findFirstResponderInViewHierarchy(tableView),
let pointInTable = firstResponder.superview?.convert(firstResponder.frame.origin, to: tableView) else {
return
}
mainResponder = firstResponder
var contentOffset = tableView.contentOffset
contentOffset.y = (pointInTable.y - (firstResponder.inputAccessoryView?.frame.size.height ?? 0)) - 10
tableView.setContentOffset(contentOffset, animated: true)
} else if let view = self.view {
guard let firstResponder = self.findFirstResponderInViewHierarchy(view) else {
return
}
mainResponder = firstResponder
var aRect = view.frame
aRect.size.height -= kbRect.size.height
if (!aRect.contains(firstResponder.frame.origin) ) {
UIView.animate(withDuration: 0.1) {
view.transform = CGAffineTransform(translationX: 0, y: -kbRect.size.height)
}
}
}
if let block = self.didReceiveKeyboardEvent {
block(kbRect.size, mainResponder, .show)
}
})
}
private func registerForKeyboardWillHideNotification() {
_ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
guard let `self` = self else { return }
guard let userInfo = notification.userInfo else { return }
guard let kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
let contentInsets = UIEdgeInsets.zero
guard self.topViewHasCurrenView() else { return }
if let scrollView = self.view as? UIScrollView {
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
} else if let tableView = self.view as? UITableView {
tableView.contentInset = contentInsets
tableView.scrollIndicatorInsets = contentInsets
tableView.contentOffset = CGPoint(x: 0, y: 0)
} else if let view = self.view {
view.transform = CGAffineTransform(translationX: 0, y: 0)
}
if let block = self.didReceiveKeyboardEvent {
block(kbRect.size, nil, .hide)
}
})
}
}
//MARK: - UIGestureRecognizerDelegate
extension KeyboardManager: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view is UIControl ||
touch.view is UINavigationBar { return false }
return true
}
}
Answer from #Mikhail worked very well; it just has the issue that it cannot support dragging to select text within TextView - keyboard will close on tapping on the selected text. I extended his solution for AnyGesture below to provide better text editing user experience. (Answer from How to check for a UITextRangeView?)
Any recommendations to optimise the while loop?
class AnyGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touchedView = touches.first?.view, touchedView is UIControl {
state = .cancelled
} else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable {
state = .cancelled
} else {
// Check if it is a subview of editable UITextView
if var touchedView = touches.first?.view {
while let superview = touchedView.superview {
if let view = superview as? UITextView, view.isEditable {
state = .cancelled
return
} else {
touchedView = superview
}
}
}
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}
extension UIView{
override open func touchesBegan(_ touches: Set, with event: UIEvent?) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}