145

I have seven TextField inside my main ContentView. When user open keyboard some of the TextField are hidden under the keyboard frame. So I want to move all TextField up respectively when the keyboard has appeared.

I have used the below code to add TextField on the screen.

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
            VStack {
                TextField($textfieldText, placeholder: Text("TextField1"))
                TextField($textfieldText, placeholder: Text("TextField2"))
                TextField($textfieldText, placeholder: Text("TextField3"))
                TextField($textfieldText, placeholder: Text("TextField4"))
                TextField($textfieldText, placeholder: Text("TextField5"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField7"))
            }
    }
}

Output:

Output

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
Hitesh Surani
  • 12,733
  • 6
  • 54
  • 65
  • You may use ScrollView. https://developer.apple.com/documentation/swiftui/scrollview – Prashant Tukadiya Jun 07 '19 at 09:52
  • 1
    @PrashantTukadiya Thanks for the quick response. I have added TextField inside Scrollview but still facing the same issue. – Hitesh Surani Jun 07 '19 at 09:58
  • 1
    @DimaPaliychuk This won't work. it is SwiftUI – Prashant Tukadiya Jun 07 '19 at 10:13
  • @DimaPaliychuk. IQKeyboardManager is not worked with SwiftUI. It only works with UIKit based component. By the way thanks for replay :D – Hitesh Surani Jun 07 '19 at 10:17
  • No padding, ScrollView or List views are actually necessary. I posted my answer with two full examples that not only moves the view, but it also checks where the textfields are to determine if the move is actually needed. and it only moves it enough to make the textfield unhidden and not a pixel more. – kontiki Jun 23 '19 at 05:30
  • 118
    The showing of the keyboard and it obscuring content on the screen has been around since what, the first Objective C iPhone app? This is problem that is *constantly* being solved. I for one am disappointed that Apple has not addressed this with SwiftUi. I know this comment is not helpful to anyone, but I wanted to raise this issue that we really should be putting pressure on Apple to provide a solution and not rely on the community to always supply this most common of problems. – P. Ent Nov 12 '19 at 17:00
  • 6
    There is a very good article by Vadim https://www.vadimbulavin.com/how-to-move-swiftui-view-when-keyboard-covers-text-field/ – Sudara Apr 26 '20 at 23:52
  • go down to the one with over 25 up votes AdaptsToKeyboard – Dave Kozikowski Jun 04 '20 at 21:55
  • What @DaveKozikowski said: https://stackoverflow.com/a/60178361/2660216 --- Very easy to implement, works in many different cases without issues, including a nice animation! – P Kuijpers Aug 05 '20 at 11:59

29 Answers29

97

I tried many of the proposed solutions, and even though they work in most cases, I had some issues - mainly with safe area (I have a Form inside TabView's tab).

I ended up combining few different solutions, and using GeometryReader in order to get specific view's safe area bottom inset and use it in padding's calculation:

import SwiftUI
import Combine

struct AdaptsToKeyboard: ViewModifier {
    @State var currentHeight: CGFloat = 0
    
    func body(content: Content) -> some View {
        GeometryReader { geometry in
            content
                .padding(.bottom, self.currentHeight)
                .onAppear(perform: {
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillShowNotification)
                        .merge(with: NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillChangeFrameNotification))
                        .compactMap { notification in
                            withAnimation(.easeOut(duration: 0.16)) {
                                notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
                            }
                    }
                    .map { rect in
                        rect.height - geometry.safeAreaInsets.bottom
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
                    
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillHideNotification)
                        .compactMap { notification in
                            CGFloat.zero
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
                })
        }
    }
}

extension View {
    func adaptsToKeyboard() -> some View {
        return modifier(AdaptsToKeyboard())
    }
}

Usage:

struct MyView: View {
    var body: some View {
        Form {...}
        .adaptsToKeyboard()
    }
}
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
Predrag Samardzic
  • 2,661
  • 15
  • 18
  • 8
    Wow, this is the most SwiftUI version of them all, with GeometryReader and ViewModifier. Love it. – Mateusz Mar 02 '20 at 14:49
  • 1
    This worked perfectly for me, I have one question, how do I move the keyboard down like 10 more? It's ever so slightly covering the bottom line of my TextField, I'm fairly new to swift so I couldn't figure it out! – Oliver Mar 19 '20 at 10:42
  • @Oliver what phone size are you having issues with? I made sure to add padding before the background color of the TextField. In addition the textField is inside an HStack. – Aaron A Mar 29 '20 at 18:28
  • 3
    This is so useful and elegant. Thank you so much for writing this up. – Danilo Campos Mar 31 '20 at 18:36
  • 6
    I am seeing a small blank white view over my keyboard. This view is GeometryReader View, i confirmed by changing background color. Any idea why GeometryReader is showing between my actual View and keyboard. – user832 Apr 06 '20 at 09:14
  • 6
    I'm getting the error `Thread 1: signal SIGABRT` on line `rect.height - geometry.safeAreaInsets.bottom` when I go to the view with the keyboard a second time and click the `TextField`. It doesn't matter if I click the `TextField` the first time or not. The app still crashes. – JLively May 08 '20 at 13:10
  • This actually is one of the simpler solutions, but how could we match the view's animation with the keyboard's? Like the view is shrining faster than the keyboard is coming up (since you are manually applying an animation duration value). – Zorayr Jun 04 '20 at 20:22
  • 1
    On view disappear, `rect.height - geometry.safeAreaInsets.bottom` is crashing; not sure what's going on since the crash is from `GeometryProxy.safeAreaInsets.getter ()` – Zorayr Jun 04 '20 at 21:05
  • 2
    Finally Something that works! Why Apple cant to this for us is nuts!! – Dave Kozikowski Jun 04 '20 at 21:54
  • I'm on iOS 14 beta 3 and I had the same problem than @user832. It seems that the safeareainset is updated when the keyboard appears and disappears, but the view does not notice it. I made some changes, eliminate the GeometryReader and just change the padding to 0.1 so the view is required to update. It's working for me now. – Joel Jul 24 '20 at 22:21
  • Unfortunately it does have a runtime issue on iPad when the app runs as "picture in picture" similar to Michael Neas's solution. But overall, great improvement. – Sorin Dolha Aug 06 '20 at 15:15
  • Amazing solution! – dankito Aug 10 '20 at 13:17
  • Works perfectly for my `VStack` (in a `NavigationView`), containing content like `ScrollView` and `HStack`, and all this with a UIKit tab bar at the bottom. Rushing a project, so you have my thanks. – Cloud Aug 15 '20 at 11:59
  • 2
    Crashes on the safeAreaInsets seem to happen on iOS13 only. @JLively, try this bit of code: ```if #available(iOS 14, *) { return rect.height - geometry.safeAreaInsets.bottom // on iOS 13 the safeAreaInsets are nil and causes a crash. Apparently for now it still works well iOS 14. } else { return rect.height }``` (Predrag, could you update the answer if you agree?) – P Kuijpers Aug 27 '20 at 08:04
  • @hitesh-surani This should be the right answer. Works in all situations and is really easy to implement. – Lachezar Todorov Sep 01 '20 at 12:45
  • I found this code does work well on iOS 13.3 but with iOS 13.1 and iOS 13 it gives complete WHITE view. I made my min target as 13.3. For iOS 14. The scroll and padding works out of the box, no need for the modifier. – Abdullah Sep 11 '20 at 11:50
  • 4
    You can achieve this [with **JUST ONE LINE OF CODE** from iOS 14](https://stackoverflow.com/a/63577557/5623035) – Mojtaba Hosseini Sep 18 '20 at 17:54
  • 1
    This is so well written, it's depressing when I compare with my code – GrandSteph Jan 19 '21 at 17:32
  • how to hide this view when keyboard disappears – Rahul Gaur May 10 '21 at 10:35
  • Related to the crash, while this gives a compiler warning since it shouldn't be `nil`, on iOS 13 I was able to use this safely: ``` if geometry.safeAreaInsets != nil { return rect.height - geometry.safeAreaInsets.bottom } else { return rect.height } ``` – LordParsley Aug 26 '21 at 14:16
  • 5
    Cool but don't work with ScrollView, I got big form here – Lukasz D Nov 22 '21 at 19:43
  • 1
    I am seeing same problem as @user832 i.e small blank white view over my keyboard in iOS 14.4 ..any solution ? – tp2376 Feb 22 '22 at 16:39
  • 1
    @tp2376 i might be late with my answer, but that spacing was because of default spacing from Container Stacks like VStack or HStack. If you set it to 0.0, this will resolve you blank white space issue – user832 Aug 25 '22 at 14:37
83

Code updated for the Xcode, beta 7.

You do not need padding, ScrollViews or Lists to achieve this. Although this solution will play nice with them too. I am including two examples here.

The first one moves all textField up, if the keyboard appears for any of them. But only if needed. If the keyboard doesn't hide the textfields, they will not move.

In the second example, the view only moves enough just to avoid hiding the active textfield.

Both examples use the same common code found at the end: GeometryGetter and KeyboardGuardian

First Example (show all textfields)

When the keyboard is opened, the 3 textfields are moved up enough to keep then all visible

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 1)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("enter text #1", text: $name[0])
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("enter text #2", text: $name[1])
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("enter text #3", text: $name[2])
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

        }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }

}

Second Example (show only the active field)

When each text field is clicked, the view is only moved up enough to make the clicked text field visible.

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 3)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("text #1", text: $name[0], onEditingChanged: { if $0 { self.kGuardian.showField = 0 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

            TextField("text #2", text: $name[1], onEditingChanged: { if $0 { self.kGuardian.showField = 1 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[1]))

            TextField("text #3", text: $name[2], onEditingChanged: { if $0 { self.kGuardian.showField = 2 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[2]))

            }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }.onAppear { self.kGuardian.addObserver() } 
.onDisappear { self.kGuardian.removeObserver() }

}

GeometryGetter

This is a view that absorbs the size and position of its parent view. In order to achieve that, it is called inside the .background modifier. This is a very powerful modifier, not just a way to decorate the background of a view. When passing a view to .background(MyView()), MyView is getting the modified view as the parent. Using GeometryReader is what makes it possible for the view to know the geometry of the parent.

For example: Text("hello").background(GeometryGetter(rect: $bounds)) will fill variable bounds, with the size and position of the Text view, and using the global coordinate space.

struct GeometryGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { geometry in
            Group { () -> AnyView in
                DispatchQueue.main.async {
                    self.rect = geometry.frame(in: .global)
                }

                return AnyView(Color.clear)
            }
        }
    }
}

Update I added the DispatchQueue.main.async, to avoid the possibility of modifying the state of the view while it is being rendered.***

KeyboardGuardian

The purpose of KeyboardGuardian, is to keep track of keyboard show/hide events and calculate how much space the view needs to be shifted.

Update: I modified KeyboardGuardian to refresh the slide, when the user tabs from one field to another

import SwiftUI
import Combine

final class KeyboardGuardian: ObservableObject {
    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    public var keyboardIsHidden = true

    @Published var slide: CGFloat = 0

    var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

    }

    func addObserver() {
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)
}

func removeObserver() {
 NotificationCenter.default.removeObserver(self)
}

    deinit {
        NotificationCenter.default.removeObserver(self)
    }



    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            let tfRect = self.rects[self.showField]
            let diff = keyboardRect.minY - tfRect.maxY

            if diff > 0 {
                slide += diff
            } else {
                slide += min(diff, 0)
            }

        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • 1
    Is it possible to attach `GeometryGetter` as a view modifier than a background by making it conform to `ViewModifier` protocol? – Sudara Jun 24 '19 at 23:22
  • 2
    It is possible, but what's the gain? You would be attaching it like this: ```.modifier(GeometryGetter(rect: $kGuardian.rects[1]))``` instead of ```.background(GeometryGetter(rect: $kGuardian.rects[1]))```. Not much of a difference (only 2 characters less). – kontiki Jun 25 '19 at 05:09
  • For me the animation didn't work until I **wrapped my Content in a ScrollView**. Just putting it our there if anyone has similar issues.. also for Beta 4 you need to: - update didChange -> willChange - change basic -> easeInOut or a other animation – thisIsTheFoxe Jul 27 '19 at 17:38
  • GeometryGetter doesn't compile in beta 5 - the problems start with ShapeView being an undeclared type (it's deprecated and now gone). I'm sure I'm missing something simple, but I can't see how to fix it – Feldur Sep 10 '19 at 16:25
  • @Feldur I updated the GeometryGetter code. It's an old question, and I forgot about the changes in beta 5. – kontiki Sep 10 '19 at 16:32
  • I'm getting there! ObjectBinding is gone too. I tried ObservedObject, but that doesn't work either. (Thanks for the quick response!) – Feldur Sep 10 '19 at 16:45
  • I changed ObjectBinding to ObservedObject, and then removed from the line '''struct GeometryGetter: View {'''. That compiles, but tests don't work. I have split the overall view into subviews, and passed the guardian from the parent to the subviews. That said, there's no scrolling when the keyboard appears. – Feldur Sep 10 '19 at 17:03
  • I updated the entire code. It is not compatible with the latest beta. (btw, the Content was a typo on my part, sorry). – kontiki Sep 10 '19 at 17:23
  • It's certainly not!! Oh well. Perhaps it's a bug they'll fix. Separately, rather than hard code indices into the rects, I made up an enum of type Int, letting me code them symbolically (that is, as RectsEnum.somecase.rawValue, naming should undoubtedly vary). I named the last case count, and use it for the array sizing. It was really bothering me to have to both code each index in two places, get the count right, and not overlap. The enum solves all that. – Feldur Sep 11 '19 at 01:07
  • @kontiki - wondering: did you think about using preferences to feed the size back up the view stack instead of the delayed store into the rect using the DispatchQueue.main.async call? (Overall, I'm trying to make this work with nesting NavigationView->Form->Section->TextField. I'm hoping this is something Apple will natively see the need to fix robustly enough that works) – Feldur Sep 11 '19 at 01:53
  • @Feldur yes, using preferences is probably preferred ;-) (although there's nothing wrong with the current approach). However, preferences are much cleaner. When I answered this question (ages ago), I was still investigating how Preferences worked, and there was zero info about them. That's why I decided to go for the GeometryGetter solution. – kontiki Sep 11 '19 at 05:15
  • For what it's worth, I changed this so that KeyboardGuardian is an ObservableObject / ObservableObjectPublisher, and had updateSlide just return the height of the keyboard along with the objectWillChange.send(). That's working with beta 8 / GM seed. I'll post things as a separate answer – Feldur Sep 12 '19 at 22:54
  • I'm not sure why, but I'm getting a weird error on the onEditingChanged closure saying it needs more context. – vicTROLLA Oct 05 '19 at 00:23
  • Using AnyView you break SwiftUI Metal acceleration benefits. I think that Benjamin Kindle reply is more correct for SwiftUI. – Nelson Cardaci Oct 22 '19 at 21:59
  • 4
    In some situations you could get a SIGNAL ABORT from the program inside the GeometryGetter when assigning the new rectangle if you are navigating away from this screen. If that happens to you just add some code to verify that the size of the geometry is greater than zero (geometry.size.width > 0 && geometry.size.height > 0) before assigning a value to self.rect – Julio Bailon Dec 10 '19 at 21:53
  • Instead of moving textField up it goes down with above logic, i think there is issue when textfield is in different view hierarchy. – Dattatray Deokar Dec 30 '19 at 11:38
  • 1
    @JulioBailon I don't know why but moving `geometry.frame` out of `DispatchQueue.main.async` helped with SIGNAL ABORT, now will test your solution. Update: `if geometry.size.width > 0 && geometry.size.height > 0` before assigning `self.rect` helped. – Roman Vasilyev Feb 03 '20 at 03:28
  • This is great! how can I add a SecureField [for passwords] to this solution? What would need to be modified. Thanks! – Dave Kozikowski Feb 19 '20 at 03:41
  • 1
    the now breaks on 13.4 on this line: self.rect = geometry.frame(in: .global) – Dave Kozikowski Mar 26 '20 at 15:53
  • 2
    this breaks for me as well on self.rect = geometry.frame(in: .global) getting SIGNAL ABORT and tried all proposed solutions to address this error – Marwan Roushdy Apr 27 '20 at 19:21
  • .offset(y: kGuardian.slide) should probably be .offset(y: -kGuardian.slide), no? – scrrr Jul 12 '20 at 09:51
  • You don't need to import combine for the KeyboardGuardian Class. – Peter Schorn Jul 20 '20 at 02:45
  • 2
    You can achieve this [with **JUST ONE LINE OF CODE** from iOS 14](https://stackoverflow.com/a/63577557/5623035) – Mojtaba Hosseini Sep 18 '20 at 17:53
  • 2
    @MojtabaHosseini That one line of code solution only works if there is a bunch of empty space in the view. In your case you have a spacer, and that spacer area ends up shrinking. If you have multiple controls and no free/shrinkable space then that solution doesn't work. – Gary Nov 28 '20 at 04:58
73

To build off of @rraphael 's solution, I converted it to be usable by today's xcode11 swiftUI support.

import SwiftUI

final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published private(set) var currentHeight: CGFloat = 0

    init(center: NotificationCenter = .default) {
        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.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
    }
}

Usage:

struct ContentView: View {
    @ObservedObject private var keyboard = KeyboardResponder()
    @State private var textFieldInput: String = ""

    var body: some View {
        VStack {
            HStack {
                TextField("uMessage", text: $textFieldInput)
            }
        }.padding()
        .padding(.bottom, keyboard.currentHeight)
        .edgesIgnoringSafeArea(.bottom)
        .animation(.easeOut(duration: 0.16))
    }
}

The published currentHeight will trigger a UI re-render and move your TextField up when the keyboard shows, and back down when dismissed. However I didn't use a ScrollView.

Michael Neas
  • 944
  • 1
  • 7
  • 15
  • 8
    I like this answer for its simplicity. I added `.animation(.easeOut(duration: 0.16))` to try to match the speed of the keyboard sliding up. – Mark Moeykens Oct 13 '19 at 15:11
  • Why have you set a max height of 340 for the keyboard? – Daniel Ryan Oct 22 '19 at 21:29
  • 1
    @DanielRyan Sometimes the keyboard height was returning incorrect values in the simulator. I can't seem to figure out a way to pin down the problem currently – Michael Neas Oct 23 '19 at 20:04
  • 1
    I haven't seen that issue myself. Maybe it's fixed in the latest versions. I didn't want to lock down the size in case there are (or will be) larger keyboards. – Daniel Ryan Oct 24 '19 at 20:54
  • 1
    You could try with `keyboardFrameEndUserInfoKey`. That should hold the final frame for the keyboard. – Mathias Claassen Nov 01 '19 at 17:00
  • @MathiasClaassen thank you! Updating my answer because that key is much better. – Michael Neas Nov 02 '19 at 17:46
  • This is generally working for me, but I'm having some issues using it in a TabView. There seems to be extra padding above the keyboard the size of the tab bar. I can work around it for testing on my iPhone 8 specifically by subtracting 49 from the keyboardSize.height, but that doesn't scale across devices. Does anyone know how to get the tab bar height programmatically? – lmh Nov 17 '19 at 22:42
  • @Imh You could use a GeometryReader and subtract the height of the safeAreaInsets when you setting the padding. Like this: `.padding(.bottom, self.keyboard.currentHeight == 0 ? self.keyboard.currentHeight : self.keyboard.currentHeight - geometry.safeAreaInsets.bottom)` – stefOCDP Nov 28 '19 at 19:37
  • I like this solution, but how would we scroll a little higher so the focused text field is a bit higher above the keyboard? Right now it is right on the edge of the keyboard and doesn't look good that way. – ajbraus Apr 24 '20 at 21:30
  • This solution only returns the height of the keyboard to help offset by that height. @kontiki solution (although longer) provides a slide value that will update depending on wether or not the keyboard is over the textField so you don't offset your view if not needed (and potentially push your textfield out of view if it was on top of screen) – GrandSteph May 01 '20 at 09:21
  • The problem with this solution is that it moves the view up even if it the text field is at the top of the screen. Also text fields at the top of the screen get moved off of the screen. – Peter Schorn Jul 20 '20 at 03:02
  • When you're on an iPad and display the app as "picture in picture" the keyboard will only use part of the bottom of the app's window. Therefore the move up will be a bit too much (and it's not because of safe area thing, but because the picture in picture window doesn't reach to the very bottom of the screen, while keyboard does). But overall, nice solution. – Sorin Dolha Aug 06 '20 at 15:01
  • To solve the issue for iPad (easy way), I just updated keyboardHeight considering the difference between window's height and screen's height: .onReceive(Publishers.keyboardHeight) { self.keyboardHeight = $0 - (UIApplication.shared.windows[0].screen.bounds.height - UIApplication.shared.windows[0].frame.height) / 2 } – Sorin Dolha Aug 07 '20 at 11:11
  • 1
    You can achieve this [with **JUST ONE LINE OF CODE** from iOS 14](https://stackoverflow.com/a/63577557/5623035) – Mojtaba Hosseini Sep 18 '20 at 17:53
  • For my use case this works like a charm. Thanks – Domenico Jan 17 '22 at 17:14
  • I am getting error 'keyboardFrameEndUserInfoKey' has been renamed to 'UIKeyboardFrameEndUserInfoKey'. When fix with this suggestion I got; "Type 'UIResponder' has no member 'UIKeyboardFrameEndUserInfoKey'" – Ammar Mujeeb Jan 04 '23 at 12:15
56

From iOS 14.2, TextFields are keyboard aware by default if they have enough space to move. For example, if it is in a VStack with a Spacer (Look at the old demo code below without the modifier)


⚠️ It seems the following code is not working as expected for +iOS 14.2

Xcode 12 (to iOS 14.2) - One line code

Add this modifier to the TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Demo

Apple added the keyboard as a region for the safe area, so you can use it to move any View with the keyboard like other regions.

Alexander Volkov
  • 7,904
  • 1
  • 47
  • 44
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • It works on **Any** `View`, including the `TextEditor`. – Mojtaba Hosseini Aug 28 '20 at 13:02
  • @MojtabaHosseini what if I want to prevent this behavior? I have an image that is now being moved up when I open the keyboard, that I do not want to move. – kyrers Sep 21 '20 at 12:54
  • You should apply it on the proper view. As you can see in this preview, the red view stays still. @kyrers – Mojtaba Hosseini Sep 21 '20 at 12:56
  • @Mojtaba Hosseini I have the exact same layout as you, except that I have a simple ``Image("x.png")`` where you have your ``TextField`` – kyrers Sep 21 '20 at 13:01
  • @MojtabaHosseini Thanks for the help, but I figured it out already. Just had to change a few things in parent View. – kyrers Sep 21 '20 at 16:57
  • @kyrers Then please tell us what you changed. Maybe there are more people having the same issue... – leonboe1 Sep 22 '20 at 11:44
  • 2
    I figured it out myself. Add ```.ignoresSafeArea(.keyboard)``` to your View. – leonboe1 Sep 22 '20 at 12:46
  • @leonboe1 sorry. I did not mention it as it was structural problems. MojtabaHosseini had already said which line of code was needed. It was just a matter of applying it to the correct view. – kyrers Sep 22 '20 at 15:04
  • @MojtabaHosseini please, can you say something about offset jumping at end to start of video? Looks like initial offset is different than after keyboard hides. – Andrey M. Oct 14 '20 at 05:56
  • 54
    This is NOT a solution. This line actually tells the compiler NOT to respect a safe area on the control (but that does nothing). Just delete the line and you will see the exact same behaviour. In iOS14 the keyboard avoidance is default. Your view will shrink to a size of the screen minus the keyboard if present. With .ignoresSafeArea you can actually PREVENT it from happening on views. Thats why it is called ignores-safe-area. – Burgler-dev Feb 05 '21 at 10:42
  • @Burgler-dev I was wondering the same that isn't this `ignoresSafeArea(.keyboard)` thing supposed to prevent the default behavior of shifting the views up. Good to see someone's the confirmation, thanks! – Orkhan Alikhanov Mar 04 '21 at 05:30
  • It works for me (.ignoresSafeArea(.keyboard, edges: .bottom)) and in this case, I can control the offset of objects and define this parameter for all elements or for each separately. – PAULMAX Feb 05 '23 at 11:36
37

I created a View that can wrap any other view to shrink it when the keyboard appears.

It's pretty simple. We create publishers for keyboard show/hide events and then subscribe to them using onReceive. We use the result of that to create a keyboard-sized rectangle behind the keyboard.

struct KeyboardHost<Content: View>: View {
    let view: Content

    @State private var keyboardHeight: CGFloat = 0

    private let showPublisher = NotificationCenter.Publisher.init(
        center: .default,
        name: UIResponder.keyboardWillShowNotification
    ).map { (notification) -> CGFloat in
        if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
            return rect.size.height
        } else {
            return 0
        }
    }

    private let hidePublisher = NotificationCenter.Publisher.init(
        center: .default,
        name: UIResponder.keyboardWillHideNotification
    ).map {_ -> CGFloat in 0}

    // Like HStack or VStack, the only parameter is the view that this view should layout.
    // (It takes one view rather than the multiple views that Stacks can take)
    init(@ViewBuilder content: () -> Content) {
        view = content()
    }

    var body: some View {
        VStack {
            view
            Rectangle()
                .frame(height: keyboardHeight)
                .animation(.default)
                .foregroundColor(.clear)
        }.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = height
        }
    }
}

You can then use the view like so:

var body: some View {
    KeyboardHost {
        viewIncludingKeyboard()
    }
}

To move the content of the view up rather than shrinking it, padding or offset can be added to view rather than putting it in a VStack with a rectangle.

Benjamin Kindle
  • 1,736
  • 12
  • 23
  • 7
    I think this is the right answer. Just a minor tweak I did: instead of a rectangle I'm just modifying the padding of `self.view` and it works great. No problems at all with the animation – Tae Aug 09 '19 at 09:35
  • 5
    Thanks! Works perfectly. As @Taed said, it's better using a padding approach. The final result would be `var body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }` – fdelafuente Aug 09 '19 at 18:35
  • 2
    Despite lesser votes this is the most swiftUIish reply. And previous approach using AnyView, breaks Metal acceleration help. – Nelson Cardaci Oct 22 '19 at 21:56
  • 5
    It's a great solution, but the main issue here is that you loose the ability to move up the view only if the keyboard is hiding the textfield you are editing. I mean: if you have a form with several textfields and you start editing the first one on top you probably don't want it to move up because it would move out of the screen. – superpuccio Dec 16 '19 at 10:52
  • I really like the answer, but like all the other answers it doesn't work if your view is inside a TabBar or the View isn't flush with the bottom of the screen. – Ben Patch Jan 08 '20 at 18:53
  • this solution moves the text field up until its disappears if the container view is a List or ScrollView – JAHelia Feb 24 '20 at 10:10
  • Keyboard height is of little value as it doesn't provide the amount of offset to apply to the view that needs to move. @kontiki solution uses the frame size of the keyboard within the global coordinate space to offer a proper offset and only move the view if the keyboard is on top of your textfield. – GrandSteph May 01 '20 at 09:28
31

I have created a really simple to use view modifier.

Add a Swift file with the code below and simply add this modifier to your views:

.keyboardResponsive()
import SwiftUI

struct KeyboardResponsiveModifier: ViewModifier {
  @State private var offset: CGFloat = 0

  func body(content: Content) -> some View {
    content
      .padding(.bottom, offset)
      .onAppear {
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notif in
          let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
          let height = value.height
          let bottomInset = UIApplication.shared.windows.first?.safeAreaInsets.bottom
          self.offset = height - (bottomInset ?? 0)
        }

        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { notif in
          self.offset = 0
        }
    }
  }
}

extension View {
  func keyboardResponsive() -> ModifiedContent<Self, KeyboardResponsiveModifier> {
    return modifier(KeyboardResponsiveModifier())
  }
}

jberlana
  • 455
  • 4
  • 11
  • 3
    Would be cool, if it would only offset, if necessary (i.e. don't scroll, if the keyboard doesn't cover the input element). Nice to have... – decades Jan 05 '20 at 21:28
  • This works great, thank you. Very clean implementation as well, and for me, only scrolls if required. – Joshua Feb 12 '20 at 09:12
  • 1
    Awesome! Why don't you provide this on Github or elsewhere? :) Or you could suggest this to https://github.com/hackiftekhar/IQKeyboardManager as they do not have a full SwiftUI support yet – Schnodderbalken Feb 21 '20 at 12:07
  • 1
    Won't play nice with orientation changes and will offset regardless of if it's needed or not. – GrandSteph May 01 '20 at 09:30
  • One issue here is that this is not animating at all... creates a very jittery motion – Zorayr Jun 04 '20 at 20:24
29

Or You can just use IQKeyBoardManagerSwift

and can optionally add this to your app delegate to hide the toolbar and enable hiding of keyboard on click on any view other then keyboard.

IQKeyboardManager.shared.enableAutoToolbar = false
IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide
aturan23
  • 4,798
  • 4
  • 28
  • 52
Amit Samant
  • 13,257
  • 2
  • 10
  • 16
  • This is indeed the (unexpected) way for me, too. Solid. – HelloTimo Feb 27 '20 at 22:14
  • This framework worked even better than expected. Thank you for sharing! – Richard Poutier Mar 24 '20 at 22:15
  • 3
    Working ok for me on SwiftUI - thanks @DominatorVbN - I on iPad landscape mode I needed to increase `IQKeyboardManager.shared.keyboardDistanceFromTextField` to 40 to get comfortable gap. – Richard Groves May 02 '20 at 14:54
  • 1
    Also had to set `IQKeyboardManager.shared.enable = true` to keep keyboard from hiding my text fields. In any case this is the best solution. I have 4 fields arranged vertically and the other solutions would work for my bottom-most field, but would push the top-most out of view. – MisterEd Jul 24 '20 at 11:52
17

I reviewed and refactored the existing solutions into a handy SPM package that provides a .keyboardAware() modifier:

KeyboardAwareSwiftUI

Example:

struct KeyboardAwareView: View {
    @State var text = "example"

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(0 ..< 20) { i in
                        Text("Text \(i):")
                        TextField("Text", text: self.$text)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .padding(.bottom, 10)
                    }
                }
                .padding()
            }
            .keyboardAware()  // <--- the view modifier
            .navigationBarTitle("Keyboard Example")
        }

    }
}

Source:

import UIKit
import SwiftUI

public class KeyboardInfo: ObservableObject {

    public static var shared = KeyboardInfo()

    @Published public var height: CGFloat = 0

    private init() {
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIApplication.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }

    @objc func keyboardChanged(notification: Notification) {
        if notification.name == UIApplication.keyboardWillHideNotification {
            self.height = 0
        } else {
            self.height = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
        }
    }

}

struct KeyboardAware: ViewModifier {
    @ObservedObject private var keyboard = KeyboardInfo.shared

    func body(content: Content) -> some View {
        content
            .padding(.bottom, self.keyboard.height)
            .edgesIgnoringSafeArea(self.keyboard.height > 0 ? .bottom : [])
            .animation(.easeOut)
    }
}

extension View {
    public func keyboardAware() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAware())
    }
}
Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
15

You need to add a ScrollView and set a bottom padding of the size of the keyboard so the content will be able to scroll when the keyboard appears.

To get the keyboard size, you will need to use the NotificationCenter to register for keyboards event. You can use a custom class to do so:

import SwiftUI
import Combine

final class KeyboardResponder: BindableObject {
    let didChange = PassthroughSubject<CGFloat, Never>()

    private var _center: NotificationCenter
    private(set) var currentHeight: CGFloat = 0 {
        didSet {
            didChange.send(currentHeight)
        }
    }

    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) {
        print("keyboard will show")
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        print("keyboard will hide")
        currentHeight = 0
    }
}

The BindableObject conformance will allow you to use this class as a State and trigger the view update. If needed look at the tutorial for BindableObject: SwiftUI tutorial

When you get that, you need to configure a ScrollView to reduce its size when the keyboard appear. For convenience I wrapped this ScrollView into some kind of component:

struct KeyboardScrollView<Content: View>: View {
    @State var keyboard = KeyboardResponder()
    private var content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        ScrollView {
            VStack {
                content
            }
        }
        .padding(.bottom, keyboard.currentHeight)
    }
}

All you have to do now is to embed your content inside the custom ScrollView.

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
        KeyboardScrollView {
            ForEach(0...10) { index in
                TextField(self.$textfieldText, placeholder: Text("TextField\(index)")) {
                    // Hide keyboard when uses tap return button on keyboard.
                    self.endEditing(true)
                }
            }
        }
    }

    private func endEditing(_ force: Bool) {
        UIApplication.shared.keyWindow?.endEditing(true)
    }
}

Edit: The scroll behaviour is really weird when the keyboard is hiding. Maybe using an animation to update the padding would fix this, or you should consider using something else than the padding to adjust the scroll view size.

Daniel
  • 8,794
  • 4
  • 48
  • 71
rraphael
  • 10,041
  • 2
  • 25
  • 33
  • hey it seems you have experience in bindableobject. I can't get it working as I want. It would be nice if you could look over: https://stackoverflow.com/questions/56500147/how-to-use-bindableobjects-enviromentobject – SwiftiSwift Jun 07 '19 at 20:10
  • Why aren't you using @ObjectBinding – SwiftiSwift Jun 11 '19 at 22:27
  • 3
    With `BindableObject` deprecated, this is not working anymore, unfortunately. – LinusGeffarth Sep 09 '19 at 21:31
  • 2
    @LinusGeffarth For what it's worth, `BindableObject` was just renamed to `ObservableObject`, and `didChange` to `objectWillChange`. The object updates the view just fine (though I tested using `@ObservedObject` instead of `@State`) – AverageHelper Oct 23 '19 at 21:54
  • 2
    Hi, this solution is scrolling the content, but it show some white area above keyboard which hides half of textfield. Please let me know how we can remove the white area. – Shahbaz Sajjad Sep 21 '20 at 18:13
  • Nice solution! There is one slight improvement you can make there. Instead of using `UIResponder.keyboardFrameBeginUserInfoKey` your should instead use `UIResponder.keyboardFrameEndUserInfoKey`. With that change the keyboard size does not vary when appearing. See this post for reference: [keyboard height varies when appearing](https://stackoverflow.com/questions/42358783/keyboard-height-varies-when-appearing) – mufumade Apr 16 '21 at 18:51
6

A few of the solutions above had some issues and weren't necessarily the "cleanest" approach. Because of this, I've modified a few things for the implementation below.

extension View {
    func onKeyboard(_ keyboardYOffset: Binding<CGFloat>) -> some View {
        return ModifiedContent(content: self, modifier: KeyboardModifier(keyboardYOffset))
    }
}

struct KeyboardModifier: ViewModifier {
    @Binding var keyboardYOffset: CGFloat
    let keyboardWillAppearPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    let keyboardWillHidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)

    init(_ offset: Binding<CGFloat>) {
        _keyboardYOffset = offset
    }

    func body(content: Content) -> some View {
        return content.offset(x: 0, y: -$keyboardYOffset.wrappedValue)
            .animation(.easeInOut(duration: 0.33))
            .onReceive(keyboardWillAppearPublisher) { notification in
                let keyWindow = UIApplication.shared.connectedScenes
                    .filter { $0.activationState == .foregroundActive }
                    .map { $0 as? UIWindowScene }
                    .compactMap { $0 }
                    .first?.windows
                    .filter { $0.isKeyWindow }
                    .first

                let yOffset = keyWindow?.safeAreaInsets.bottom ?? 0

                let keyboardFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? .zero

                self.$keyboardYOffset.wrappedValue = keyboardFrame.height - yOffset
        }.onReceive(keyboardWillHidePublisher) { _ in
            self.$keyboardYOffset.wrappedValue = 0
        }
    }
}
struct RegisterView: View {
    @State var name = ""
    @State var keyboardYOffset: CGFloat = 0

    var body: some View {

        VStack {
            WelcomeMessageView()
            TextField("Type your name...", text: $name).bordered()
        }.onKeyboard($keyboardYOffset)
            .background(WelcomeBackgroundImage())
            .padding()
    }
}

I would have liked a cleaner approach and to move responsibility to the constructed view (not the modifier) in how to offset the content, but it would seem I couldn't get the publishers to properly trigger when moving the offset code to the view....

Also note that Publishers had to be used in this instance as final class currently causes unknown exception crashes (even though it meets interface requirements) and a ScrollView overall is the best approach when applying offset code.

TheCodingArt
  • 3,436
  • 4
  • 30
  • 53
  • Very nice solution, highly recommend! I added a Bool to indicate whether the keyboard was currently active. – Peanutsmasher Jun 15 '20 at 20:35
  • Best and easiest solution, highly recommended! – Peter Kreinz Mar 29 '21 at 22:27
  • Does it work? For me complete view goes off screen. – Santosh Singh Feb 19 '22 at 09:12
  • @SantoshSingh I recommend reading code and understanding what it does rather a vanilla copy paste. Not understanding the code or what it does while blindly taking it is not a good habit to be in… – TheCodingArt Feb 19 '22 at 16:49
  • Is it possible to allow scrolling thru all the form fields when the keyboard is visible with this solution? I've tried but couldn't get it to work correctly. as mentioned above the view does go off screen. – Wael Dec 07 '22 at 04:23
6

Usage:

import SwiftUI

var body: some View {
    ScrollView {
        VStack {
          /*
          TextField()
          */
        }
    }.keyboardSpace()
}

Code:

import SwiftUI
import Combine

let keyboardSpaceD = KeyboardSpace()
extension View {
    func keyboardSpace() -> some View {
        modifier(KeyboardSpace.Space(data: keyboardSpaceD))
    }
}

class KeyboardSpace: ObservableObject {
    var sub: AnyCancellable?
    
    @Published var currentHeight: CGFloat = 0
    var heightIn: CGFloat = 0 {
        didSet {
            withAnimation {
                if UIWindow.keyWindow != nil {
                    //fix notification when switching from another app with keyboard
                    self.currentHeight = heightIn
                }
            }
        }
    }
    
    init() {
        subscribeToKeyboardEvents()
    }
    
    private let keyboardWillOpen = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .map { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { $0.height - (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0) }
    
    private let keyboardWillHide =  NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in CGFloat.zero }
    
    private func subscribeToKeyboardEvents() {
        sub?.cancel()
        sub = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
            .subscribe(on: RunLoop.main)
            .assign(to: \.self.heightIn, on: self)
    }
    
    deinit {
        sub?.cancel()
    }
    
    struct Space: ViewModifier {
        @ObservedObject var data: KeyboardSpace
        
        func body(content: Content) -> some View {
            VStack(spacing: 0) {
                content
                
                Rectangle()
                    .foregroundColor(Color(.clear))
                    .frame(height: data.currentHeight)
                    .frame(maxWidth: .greatestFiniteMagnitude)

            }
        }
    }
}

extension UIWindow {
    static var keyWindow: UIWindow? {
        let keyWindow = UIApplication.shared.connectedScenes
            .first { $0.activationState == .foregroundActive }
            .flatMap { $0 as? UIWindowScene }?.windows
            .first { $0.isKeyWindow }
        return keyWindow
    }
}
Aviel Gross
  • 9,770
  • 3
  • 52
  • 62
8suhas
  • 1,460
  • 11
  • 20
  • tried your solution... view is scrolled only to half of the textfield. Tried all the above the solution. Got the same issue. Please Help!!! – Sona May 18 '20 at 07:23
  • @Zeona, try in a simple app, you might be doing something different. Also, try removing '- (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0)' if you are using safe area. – 8suhas May 18 '20 at 13:33
  • removed (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0) then I am getting a white space above keyboard – Sona May 19 '20 at 04:46
  • It's worked for me in SwiftUI. thank you :) – vipinsaini0 Jul 05 '21 at 05:45
  • This is awesome! Just made a tiny edit to the UIWindow extension (: – Aviel Gross Mar 21 '22 at 12:44
4

This is adapted from what @kontiki built. I have it running in an app under beta 8 / GM seed, where the field needing scrolled is part of a form inside a NavigationView. Here's KeyboardGuardian:

//
//  KeyboardGuardian.swift
//
//  https://stackoverflow.com/questions/56491881/move-textfield-up-when-thekeyboard-has-appeared-by-using-swiftui-ios
//

import SwiftUI
import Combine

/// The purpose of KeyboardGuardian, is to keep track of keyboard show/hide events and
/// calculate how much space the view needs to be shifted.
final class KeyboardGuardian: ObservableObject {
    let objectWillChange = ObservableObjectPublisher() // PassthroughSubject<Void, Never>()

    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    private var keyboardIsHidden = true

    var slide: CGFloat = 0 {
        didSet {
            objectWillChange.send()
        }
    }

    public var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)

    }

    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            slide = -keyboardRect.size.height
        }
    }
}

Then, I used an enum to track the slots in the rects array and the total number:

enum KeyboardSlots: Int {
    case kLogPath
    case kLogThreshold
    case kDisplayClip
    case kPingInterval
    case count
}

KeyboardSlots.count.rawValue is the necessary array capacity; the others as rawValue give the appropriate index you'll use for .background(GeometryGetter) calls.

With that set up, views get at the KeyboardGuardian with this:

@ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: SettingsFormBody.KeyboardSlots.count.rawValue)

The actual movement is like this:

.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1))

attached to the view. In my case, it's attached to the entire NavigationView, so the complete assembly slides up as the keyboard appears.

I haven't solved the problem of getting a Done toolbar or a return key on a decimal keyboard with SwiftUI, so instead I'm using this to hide it on a tap elsewhere:

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

You attach it to a view as

.modifier(DismissingKeyboard())

Some views (e.g., pickers) don't like having that attached, so you may need to be somewhat granular in how you attach the modifier rather than just slapping it on the outermost view.

Many thanks to @kontiki for the hard work. You'll still need his GeometryGetter above (nope, I didn't do the work to convert it to use preferences either) as he illustrates in his examples.

Feldur
  • 1,121
  • 9
  • 23
  • 2
    To the individual who downvoted: why? I attempted to add something useful, so I'd like to know how in your view I went wrong – Feldur Sep 27 '19 at 17:16
4

I used Benjamin Kindle's answer as as starting point, but I had a few issues I wanted to address.

  1. Most of the answers here do not deal with the keyboard changing its frame, so they break if the user rotates the device with the keyboard onscreen. Adding keyboardWillChangeFrameNotification to the list of notifications processed addresses this.
  2. I didn't want multiple publishers with similar-but-different map closures, so I chained all three keyboard notifications into a single publisher. It's admittedly a long chain but each step is pretty straightforward.
  3. I provided the init function that accepts a @ViewBuilder so that you can use the KeyboardHost view like any other View and simply pass your content in a trailing closure, as opposed to passing the content view as a parameter to init.
  4. As Tae and fdelafuente suggested in comments I swapped out the Rectangle for adjusting the bottom padding.
  5. Instead of using the hard-coded "UIKeyboardFrameEndUserInfoKey" string I wanted to use the strings provided in UIWindow as UIWindow.keyboardFrameEndUserInfoKey.

Pulling that all together I have:

struct KeyboardHost<Content>: View  where Content: View {
    var content: Content

    /// The current height of the keyboard rect.
    @State private var keyboardHeight = CGFloat(0)

    /// A publisher that combines all of the relevant keyboard changing notifications and maps them into a `CGFloat` representing the new height of the
    /// keyboard rect.
    private let keyboardChangePublisher = NotificationCenter.Publisher(center: .default,
                                                                       name: UIResponder.keyboardWillShowNotification)
        .merge(with: NotificationCenter.Publisher(center: .default,
                                                  name: UIResponder.keyboardWillChangeFrameNotification))
        .merge(with: NotificationCenter.Publisher(center: .default,
                                                  name: UIResponder.keyboardWillHideNotification)
            // But we don't want to pass the keyboard rect from keyboardWillHide, so strip the userInfo out before
            // passing the notification on.
            .map { Notification(name: $0.name, object: $0.object, userInfo: nil) })
        // Now map the merged notification stream into a height value.
        .map { ($0.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height }
        // If you want to debug the notifications, swap this in for the final map call above.
//        .map { (note) -> CGFloat in
//            let height = (note.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height
//
//            print("Received \(note.name.rawValue) with height \(height)")
//            return height
//    }

    var body: some View {
        content
            .onReceive(keyboardChangePublisher) { self.keyboardHeight = $0 }
            .padding(.bottom, keyboardHeight)
            .animation(.default)
    }

    init(@ViewBuilder _ content: @escaping () -> Content) {
        self.content = content()
    }
}

struct KeyboardHost_Previews: PreviewProvider {
    static var previews: some View {
        KeyboardHost {
            TextField("TextField", text: .constant("Preview text field"))
        }
    }
}

Timothy Sanders
  • 206
  • 3
  • 7
  • this solution doesn't work, it increases `Keyboard` height – GSerjo Sep 22 '19 at 04:21
  • Can you elaborate on the problems you're seeing @GSerjo? I'm using this code in my app and it's working fine for me. – Timothy Sanders Sep 22 '19 at 18:34
  • Could you please turn on `Pridictive` in iOS `keyboard`. `Settings` -> `General` -> `Keyboard` -> `Pridictive`. in this case it doesn't correct calclate and adds padding to the keyboard – GSerjo Sep 23 '19 at 04:27
  • @GSerjo: I have Predictive text enabled on an iPad Touch (7th gen) running the iOS 13.1 beta. It correctly adds padding for the height of the prediction row. (Important to note, I'm not adjusting the height of the *keyboard* here, I'm adding to the *padding* of the view itself.) Try swapping in the debugging map that is commented out and play with the values you get for the predictive keyboard. I'll post a log in another comment. – Timothy Sanders Sep 24 '19 at 23:06
  • With the "debugging" map uncommented you can see the value being assigned to `keyboardHeight`. On my iPod Touch (in portrait) a keyboard with predictive on is 254 points. Without it is 216 points. I can even turn off predictive with a keyboard onscreen and the padding updates properly. Adding a keyboard with predictive: `Received UIKeyboardWillChangeFrameNotification with height 254.0` `Received UIKeyboardWillShowNotification with height 254.0` When I turn off predictive text: `Received UIKeyboardWillChangeFrameNotification with height 216.0` – Timothy Sanders Sep 24 '19 at 23:12
  • `Received UIKeyboardWillChangeFrameNotification with height 260.0 Received UIKeyboardWillShowNotification with height 260.0` a text field is under the keyboard :( `.padding(.bottom, keyboardHeight)` changes keyboard `height` – GSerjo Sep 25 '19 at 06:18
  • maybe the difference is here, i.e. how we're using it. this my code `.sheet(isPresented: $showModal, onDismiss: {}){ KeyboardHost { Form() } }` – GSerjo Sep 25 '19 at 06:20
  • I can create a very simple project, just to reproduce the issue – GSerjo Sep 25 '19 at 12:47
  • If your view is taller than space remaining when the keyboard is up then something has to go under the keyboard. The fix for that is replace your top level `VStack` with a `ScrollView`. In my app if I open the view in landscape it will scroll the the view so the field with focus is just above the keyboard. Here's the start of my body: `var body: some View { KeyboardHost { ScrollView { self.editingControls()` (Apologies for the line breaks. I can't seem to put multi-line code blocks in a comment.) – Timothy Sanders Sep 25 '19 at 18:14
3

I'm not sure if the transition / animation API for SwiftUI is complete, but you could use CGAffineTransform with .transformEffect

Create an observable keyboard object with a published property like this:

    final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published var readyToAppear = false

    init(center: NotificationCenter = .default) {
        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        readyToAppear = true
    }

    @objc func keyBoardWillHide(notification: Notification) {
        readyToAppear = false
    }

}

then you could use that property to rearrange your view like this:

    struct ContentView : View {
    @State var textfieldText: String = ""
    @ObservedObject private var keyboard = KeyboardResponder()

    var body: some View {
        return self.buildContent()
    }

    func buildContent() -> some View {
        let mainStack = VStack {
            TextField("TextField1", text: self.$textfieldText)
            TextField("TextField2", text: self.$textfieldText)
            TextField("TextField3", text: self.$textfieldText)
            TextField("TextField4", text: self.$textfieldText)
            TextField("TextField5", text: self.$textfieldText)
            TextField("TextField6", text: self.$textfieldText)
            TextField("TextField7", text: self.$textfieldText)
        }
        return Group{
            if self.keyboard.readyToAppear {
                mainStack.transformEffect(CGAffineTransform(translationX: 0, y: -200))
                    .animation(.spring())
            } else {
                mainStack
            }
        }
    }
}

or simpler

VStack {
        TextField("TextField1", text: self.$textfieldText)
        TextField("TextField2", text: self.$textfieldText)
        TextField("TextField3", text: self.$textfieldText)
        TextField("TextField4", text: self.$textfieldText)
        TextField("TextField5", text: self.$textfieldText)
        TextField("TextField6", text: self.$textfieldText)
        TextField("TextField7", text: self.$textfieldText)
    }.transformEffect(keyboard.readyToAppear ? CGAffineTransform(translationX: 0, y: -50) : .identity)
            .animation(.spring())
blacktiago
  • 351
  • 2
  • 11
  • I love this answer, but I'm not quite sure where 'ScreenSize.portrait' is coming from. – Misha Stone Nov 14 '19 at 11:57
  • Hi @MishaStone thanks por choose my approach. ScreenSize.portrait is a class that I made to obtain measurements of screen base on Orientation and percentage.... but you can replace it with any value you require for your translation – blacktiago Nov 15 '19 at 12:20
3

Xcode 12 beta 4 adds a new view modifier ignoresSafeArea that you can now use to avoid the keyboard.

.ignoresSafeArea([], edges: [])

This avoids the keyboard and all safe area edges. You can set the first parameter to .keyboard if you don’t want it avoided. There are some quirks to it, at least in my view hierarchy setup, but it does seem that this is the way Apple wants us to avoid the keyboard.

Mark Krenek
  • 4,889
  • 3
  • 25
  • 17
3

As Mark Krenek and Heiko have pointed out, Apple seemed to be addressing this issue at long last in Xcode 12 beta 4. Things are moving quickly. According to the release notes for Xcode 12 beta 5 published August 18, 2020 "Form, List, and TextEditor no longer hide content behind the keyboard. (66172025)". I just download it and gave it a quick test in the beta 5 simulator (iPhone SE2) with a Form container in an app I started a a few days ago.

It now "just works" for a TextField. SwiftUI will automatically provide the appropriate bottom padding to the encapsulating Form to make room for the keyboard. And it will automatically scroll the Form up to display the TextField just above the keyboard. The ScrollView container now behaves nicely when the keyboard comes up as well.

However, as Андрей Первушин pointed out in a comment, there is a problem with TextEditor. Beta 5 & 6 will automatically provide the appropriate bottom padding to the encapsulating Form to make room for the keyboard. But it will NOT automatically scroll the Form up. The keyboard will cover the TextEditor. So unlike TextField, the user has to scroll the Form to make the TextEditor visible. I will file a bug report. Perhaps Beta 7 will fix it. So close …

https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-beta-release-notes/

Positron
  • 2,371
  • 1
  • 14
  • 8
  • i see apple release notes, tested on beta5 and beta6, TextField works, TextEditor NOT, what do i miss? @State var text = "" var body: some View { Form { Section { Text(text) .frame(height: 500) } Section { TextField("5555", text: $text) .frame(height: 50) } Section { TextEditor(text: $text) .frame(height: 120) } } } – Андрей Первушин Aug 29 '20 at 05:09
2

Answer copied from here: TextField always on keyboard top with SwiftUI

I've tried different approaches, and none of them worked for me. This one below is the only one that worked for different devices.

Add this extension in a file:

import SwiftUI
import Combine

extension View {
    func keyboardSensible(_ offsetValue: Binding<CGFloat>) -> some View {
        
        return self
            .padding(.bottom, offsetValue.wrappedValue)
            .animation(.spring())
            .onAppear {
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
                    
                    let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                    
                    let bottom = keyWindow?.safeAreaInsets.bottom ?? 0
                    
                    let value = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
                    let height = value.height
                    
                    offsetValue.wrappedValue = height - bottom
                }
                
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                    offsetValue.wrappedValue = 0
                }
        }
    }
}

In your view, you need a variable to bind offsetValue:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

  var body: some View { 
    
    VStack {
     //...       
    }
    .keyboardSensible($offsetValue)
  }
}
Jonas Deichelmann
  • 3,513
  • 1
  • 30
  • 45
VSMelo
  • 355
  • 1
  • 5
  • 2
    Just an FYI, you own the objects when calling `NotificationCenter.default.addObserver`... you need to store those and remove the observers at an appropriate time... – TheCodingArt Apr 02 '20 at 19:08
  • Hi @TheCodingArt, that's right I have tried to do that like this ( https://oleb.net/blog/2018/01/notificationcenter-removeobserver/ ) but it does not seem to work for me, any ideas? – Ray May 08 '20 at 03:31
2

A lot of these answer's just seem really bloated to be honest. If you are using SwiftUI then you may as well make use of Combine as well.

Create a KeyboardResponder as shown below, then you can use as previously demonstrated.

Updated for iOS 14.

import Combine
import UIKit

final class KeyboardResponder: ObservableObject {

    @Published var keyboardHeight: CGFloat = 0

    init() {
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .compactMap { notification in
                (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height
            }
            .receive(on: DispatchQueue.main)
            .assign(to: \.keyboardHeight)
    }
}


struct ExampleView: View {
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    @State private var text: String = ""

    var body: some View {
        VStack {
            Text(text)
            Spacer()
            TextField("Example", text: $text)
        }
        .padding(.bottom, keyboardResponder.keyboardHeight)
    }
}
Edward
  • 2,864
  • 2
  • 29
  • 39
  • I added an .animation(.easeIn) to match the animation with which the keyboard appears – Michiel Pelt Jul 11 '20 at 11:17
  • (For iOS 13 go to history of this answer) – LetsGoBrandon Jul 17 '20 at 16:23
  • 7
    Hi, .assign(to: \.keyboardHeight) is giving this error "Cannot infer key path type from context; consider explicitly specifying a root type". Please let me know the proper and clean solution for both ios 13 and ios 14. – Shahbaz Sajjad Sep 21 '20 at 18:01
  • I had to add another listener for UIResponder.keyboardWillHideNotification. Other than that - this is the only solution which worked for me. Thank you! – Antonín Karásek Jan 07 '21 at 09:26
  • 1
    A few problems: the assign should be `.assign(to: \.keyboardHeight, on: self)` (in Xcode 12.5 at least). Also, you need to monitor also for `UIResponder.keyboardWillHideNotification` as well, returning always 0 for the height when that is triggered. Screenshot: https://cln.sh/Sp6zKc – Ian Dundas Mar 17 '21 at 09:44
  • @IanDundas `on: self)` is not needed in iOS 14 - check history of answer if you need that. – Edward Mar 17 '21 at 16:35
2

As for iOS 14 (beta 4) it works quite simple:

var body: some View {
    VStack {
        TextField(...)
    }
    .padding(.bottom, 0)
}

And the size of the view adjusts to the top of the keyboard. There are certainly more refinements possible with frame(.maxHeight: ...) etc. You will figure it out.

Unfortunately the floating keyboard on iPad still causes problems when moved. But the above mentioned solutions would too, and it's still beta, I hope they will figure it out.

Thx Apple, finally!

heiko
  • 1,268
  • 1
  • 12
  • 20
2

If you are using iOS 14+ with scrollview or have the option to use scrollview.

https://developer.apple.com/documentation/swiftui/scrollviewproxy https://developer.apple.com/documentation/swiftui/scrollviewreader

Below might help

        ScrollViewReader { (proxy: ScrollViewProxy) in
            ScrollView {
                view1().frame(height: 200)
                view2().frame(height: 200)

                view3() <-----this has textfields 
                    .onTapGesture {
                        proxy.scrollTo(1, anchor: .center)
                    }
                    .id(1)

                view4() <-----this has text editor
                    .onTapGesture {
                        proxy.scrollTo(2, anchor: .center)
                    }
                    .id(2)

                view5().frame(height: 200)
                view6().frame(height: 200)
                submtButton().frame(height: 200)
            }
        }

imp part from above is

         anyView().onTapGesture {
              proxy.scrollTo(_ID, anchor: .center)
         }.id(_ID)

Hope this helps someone :)

Abhishek
  • 454
  • 3
  • 9
1

Handling TabView's

I like Benjamin Kindle's answer but it doesn't support TabViews. Here is my adjustment to his code for handling TabViews:

  1. Add an extension to UITabView to store the size of the tabView when it's frame is set. We can store this in a static variable because there is usually only one tabView in a project (if yours has more than one, then you'll need to adjust).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. You'll need to change his onReceive at the bottom of the KeyboardHost view to account for the Tab Bar's height:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. And that's it! Super simple .
Ben Patch
  • 1,213
  • 1
  • 9
  • 12
1

I took a totally different approach, by extending UIHostingController and adjusting its additionalSafeAreaInsets:

class MyHostingController<Content: View>: UIHostingController<Content> {
    override init(rootView: Content) {
        super.init(rootView: rootView)
    }

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, 
                                               selector: #selector(keyboardDidShow(_:)), 
                                               name: UIResponder.keyboardDidShowNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self, 
                                               selector: #selector(keyboardWillHide), 
                                               name: UIResponder.keyboardWillHideNotification, 
                                               object: nil)
    }       

    @objc func keyboardDidShow(_ notification: Notification) {
        guard let info:[AnyHashable: Any] = notification.userInfo,
            let frame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
                return
        }

        // set the additionalSafeAreaInsets
        let adjustHeight = frame.height - (self.view.safeAreaInsets.bottom - self.additionalSafeAreaInsets.bottom)
        self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: adjustHeight, right: 0)

        // now try to find a UIResponder inside a ScrollView, and scroll
        // the firstResponder into view
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { 
            if let firstResponder = UIResponder.findFirstResponder() as? UIView,
                let scrollView = firstResponder.parentScrollView() {
                // translate the firstResponder's frame into the scrollView's coordinate system,
                // with a little vertical padding
                let rect = firstResponder.convert(firstResponder.frame, to: scrollView)
                    .insetBy(dx: 0, dy: -15)
                scrollView.scrollRectToVisible(rect, animated: true)
            }
        }
    }

    @objc func keyboardWillHide() {
        self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
}

/// IUResponder extension for finding the current first responder
extension UIResponder {
    private struct StaticFirstResponder {
        static weak var firstResponder: UIResponder?
    }

    /// find the current first responder, or nil
    static func findFirstResponder() -> UIResponder? {
        StaticFirstResponder.firstResponder = nil
        UIApplication.shared.sendAction(
            #selector(UIResponder.trap),
            to: nil, from: nil, for: nil)
        return StaticFirstResponder.firstResponder
    }

    @objc private func trap() {
        StaticFirstResponder.firstResponder = self
    }
}

/// UIView extension for finding the receiver's parent UIScrollView
extension UIView {
    func parentScrollView() -> UIScrollView? {
        if let scrollView = self.superview as? UIScrollView {
            return scrollView
        }

        return superview?.parentScrollView()
    }
}

Then change SceneDelegate to use MyHostingController instead of UIHostingController.

When that's done, I don't need to worry about the keyboard inside my SwiftUI code.

(Note: I haven't used this enough, yet, to fully understand any implications of doing this!)

Matthew
  • 1,363
  • 11
  • 21
1

This is the way I handle the keyboard in SwiftUI. The thing to remember is that it is making the calculations on the VStack to which it is attached.

You use it on a View as a Modifier. This way:

struct LogInView: View {

  var body: some View {
    VStack {
      // Your View
    }
    .modifier(KeyboardModifier())
  }
}

So to come to this modifier, first, create an extension of UIResponder to get the selected TextField position in the VStack:

import UIKit

// MARK: Retrieve TextField first responder for keyboard
extension UIResponder {

  private static weak var currentResponder: UIResponder?

  static var currentFirstResponder: UIResponder? {
    currentResponder = nil
    UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder),
                                    to: nil, from: nil, for: nil)
    return currentResponder
  }

  @objc private func findFirstResponder(_ sender: Any) {
    UIResponder.currentResponder = self
  }

  // Frame of the superview
  var globalFrame: CGRect? {
    guard let view = self as? UIView else { return nil }
    return view.superview?.convert(view.frame, to: nil)
  }
}

You can now create the KeyboardModifier using Combine to avoid a keyboard hiding a TextField:

import SwiftUI
import Combine

// MARK: Keyboard show/hide VStack offset modifier
struct KeyboardModifier: ViewModifier {

  @State var offset: CGFloat = .zero
  @State var subscription = Set<AnyCancellable>()

  func body(content: Content) -> some View {
    GeometryReader { geometry in
      content
        .padding(.bottom, self.offset)
        .animation(.spring(response: 0.4, dampingFraction: 0.5, blendDuration: 1))
        .onAppear {

          NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .handleEvents(receiveOutput: { _ in self.offset = 0 })
            .sink { _ in }
            .store(in: &self.subscription)

          NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .map(\.userInfo)
            .compactMap { ($0?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.size.height }
            .sink(receiveValue: { keyboardHeight in
              let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
              let textFieldBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
              self.offset = max(0, textFieldBottom - keyboardTop * 2 - geometry.safeAreaInsets.bottom) })
        .store(in: &self.subscription) }
        .onDisappear {
          // Dismiss keyboard
          UIApplication.shared.windows
            .first { $0.isKeyWindow }?
            .endEditing(true)

          self.subscription.removeAll() }
    }
  }
}
Roland Lariotte
  • 2,606
  • 1
  • 16
  • 40
1

enter image description here

If you want the screen to be designed like this, Then you can use the overlays like follow.

struct LoginView: View {

var body: some View {
    
    VStack(spacing: 0) {
        
        Color.clear
            .overlay {
                LogoImageView() 
              // Here you can add your any Logo image
            }
        
        Text("Login to your account")
        
        Color.clear
        
            .overlay {
                TextFieldView()
                // Here you can add multiple text field in separate 
              VStack.
            }
           
        Text("version text")   
     }
  }
}

If you want the keyboard to be overlapped on textField, use the following code.

enter image description here

  .ignoresSafeArea(.keyboard, edges: .bottom)

add this line after parent Vstack.

Shrikant Phadke
  • 358
  • 2
  • 11
1

I faced the same scenario and issue with multiple text field scrolling. I'm not an expert but I found this solution works perfectly

import SwiftUI

struct MyView: View {
@State  var titlesArray = ["ATitle" , "BTitle" , "CTitle" , "DTitle"
                           , "ETitle" , "FTitle" , "GTitle", "HTitle", "ITitle", "JTitle", "KTitle", "LTitle", "MTitle", "NTitle", "OTitle", "PTitle", "QTitle", "RTitle", "STitle", "TTitle", "UTitle", "VTitle", "WTitle", "XTitle", "YTitle", "ZTitle"]
@State  var name = ""

@State private var isKeyboardVisible = false


var body: some View {
    
    
    
    VStack {
        ScrollViewReader { proxy in // Use a ScrollViewReader to scroll to fields
            
            ScrollView {
                LazyVStack(spacing : 20) {
                    
                    ForEach(Array(titlesArray.indices), id: \.self) { index in
                        
                        TextField("Text Field \(index+1)", text: $name, onEditingChanged: { isFocused in
                            if isFocused {
                                
                                withAnimation {
                                    proxy.scrollTo(index,anchor : .top)// scroll the selected textfield
                                    
                                }
                            }
                        })
                        .id(index) // provide the unique id for ScrollViewReader to read which text field should go on top
                        
                        
                        .frame(height: 45)
                        .padding([.leading,.trailing],20)
                        .disableAutocorrection(true)
                        .keyboardType(.alphabet)
                        .submitLabel(.return)
                        
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Colors().mheroon, lineWidth: 1)
                        )
                        .padding([.leading,.trailing],20)
                    }
                }
                .padding(.bottom, isKeyboardVisible ? 180 : 0) // to give some extra space for scorll view else last text field will not scroll on top
                
            }
        }
        .padding(.top,20)
        
        Spacer()
        
        VStack {
            Spacer()
            Button {
                
            } label: {
                Text("continue")
                    .padding()
            }
            Spacer()
            
        }
        .frame(height: 80)
        
        
        
    }
    .ignoresSafeArea(.keyboard, edges: .bottom)
    //if you provide such padding .ignoresSafeArea(.keyboard, edges: .bottom) this line of code willn't work and default scrolling will go on
    //        .padding(.top,50)
    //        .padding()
    .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
        self.isKeyboardVisible = true
    }
    .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
        self.isKeyboardVisible = false
    }
 
 }
}

struct MyView_Previews: PreviewProvider {
static var previews: some View {
    MyView()
 }
}
  • Have you tested this code? It looks ridiculous [example](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExOTcwZGIyMTQ1YmY1MmU0Mzc3MTdiMzFlMTBmNmI4MDM1YWExYTQ0NiZlcD12MV9pbnRlcm5hbF9naWZzX2dpZklkJmN0PWc/Kp8asWbcUrDYq6j8bI/giphy.gif) – Mixorok Apr 28 '23 at 11:52
  • @Mixorok yes sir, my mistake and I made some modifications please check now [Example](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExOTMzZWRiNDQ5NGEwMWE0MDlhMzAxODkzZTY5ODkzMTlkZGMyNjJlOSZlcD12MV9pbnRlcm5hbF9naWZzX2dpZklkJmN0PWc/LWnvnzqbpGvqRsQMp2/giphy.gif) – Ajay Sharma Apr 29 '23 at 12:36
0

My View:

struct AddContactView: View {
    
    @Environment(\.presentationMode) var presentationMode : Binding<PresentationMode>
    
    @ObservedObject var addContactVM = AddContactVM()
    
    @State private var offsetValue: CGFloat = 0.0
    
    @State var firstName : String
    @State var lastName : String
    @State var sipAddress : String
    @State var phoneNumber : String
    @State var emailID : String
    
  
    var body: some View {
        
        
        VStack{
            
            Header(title: StringConstants.ADD_CONTACT) {
                
                self.presentationMode.wrappedValue.dismiss()
            }
            
           ScrollView(Axis.Set.vertical, showsIndicators: false){
            
            Image("contactAvatar")
                .padding(.top, 80)
                .padding(.bottom, 100)
                //.padding(.vertical, 100)
                //.frame(width: 60,height : 60).aspectRatio(1, contentMode: .fit)
            
            VStack(alignment: .center, spacing: 0) {
                
                
                TextFieldBorder(placeHolder: StringConstants.FIRST_NAME, currentText: firstName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.LAST_NAME, currentText: lastName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.SIP_ADDRESS, currentText: sipAddress, imageName: "sipPhone")
                TextFieldBorder(placeHolder: StringConstants.PHONE_NUMBER, currentText: phoneNumber, imageName: "phoneIcon")
                TextFieldBorder(placeHolder: StringConstants.EMAILID, currentText: emailID, imageName: "email")
                

            }
            
           Spacer()
            
        }
        .padding(.horizontal, 20)
        
            
        }
        .padding(.bottom, self.addContactVM.bottomPadding)
        .onAppear {
            
            NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
            
             NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        }
        
    }
}

My VM:

class AddContactVM : ObservableObject{
    
    @Published var contact : Contact = Contact(id: "", firstName: "", lastName: "", phoneNumbers: [], isAvatarAvailable: false, avatar: nil, emailID: "")
    
    @Published var bottomPadding : CGFloat = 0.0
    
    @objc  func keyboardWillShow(_ notification : Notification){
        
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardRectangle = keyboardFrame.cgRectValue
            let keyboardHeight = keyboardRectangle.height
            self.bottomPadding = keyboardHeight
        }
        
    }
    
    @objc  func keyboardWillHide(_ notification : Notification){
        
        
        self.bottomPadding = 0.0
        
    }
    
}

Basically, Managing bottom padding based on keyboard height.

0

Here's a different approach that I had to do for making it work in iOS 15

import Combine
import UIKit

public final class KeyboardResponder: ObservableObject {

@Published public var keyboardHeight: CGFloat = 0
var showCancellable: AnyCancellable?
var hideCancellable: AnyCancellable?

public init() {
    showCancellable = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    .map { notification in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0.0
    }
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: { height in
        print(height)
        self.keyboardHeight = height
    })
    
    hideCancellable = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: { _ in
        self.keyboardHeight = 0
    })
  }
}

And then use it like this:

@StateObject private var keyboardResponder = KeyboardResponder()

SomeView
   .padding(.bottom, keyboardResponder.keyboardHeight)

It's not the cleanest solution, but I wasn't able to get 0 in return from the notification when dismissing the keyboard, so I had to split them up like this.. Hope this will help someone :)

Mixorok
  • 125
  • 9
Nicolai Harbo
  • 1,064
  • 12
  • 25
0

I've gone thru every single solution here and whilst some of them are nicely implemented, none of them worked correctly displaying half of the text field. Also none of the solutions work at all with a TextEditor control unless you offset the -y coordinate of the content which would look odd anyway. The user needs to be able to scroll thru all the form fields even when the keyboard is displayed.

The scenario is when you have a view which contains a form with a ScrollView that has a number of text fields including a Text editor field and a button that is always visible at the bottom of the form using .ignoresSafeArea(.keyboard). I am still working on this issue. If anyone has a complete solution please kindly assist.

Also I found that unfortunately when using .ignoresSafeArea(.keyboard) to make the button displayed always at the bottom if I use a ScrollViewReader in combination with any of the solutions above, scrollTo just doesn't work at all.

Wael
  • 489
  • 6
  • 19
-3

The most elegant answer I've managed to this is similar to rraphael's solution. Create a class to listen for keyboard events. Instead of using the keyboard size to modify padding though, return a negative value of the keyboard size, and use the .offset(y:) modifier to adjust the the outer most view containers's offset. It animates well enough, and works with any view.

pcallycat
  • 5
  • 5
  • How did you get this to animate? I have `.offset(y: withAnimation { -keyboard.currentHeight })`, but the content jumps instead of animates. – jjatie Jul 29 '19 at 12:53
  • It's been a few betas ago that I mucked with this code, but at the time of my earlier comment, modifying the offset of a vstack during runtime was all that was required, SwiftUI would animate the change for you. – pcallycat Aug 04 '19 at 17:38