Di chuyển TextField lên khi bàn phím đã xuất hiện trong SwiftUI


100

Tôi có bảy cái TextFieldbên trong chính của tôi ContentView. Khi người dùng mở bàn phím, một số trong số TextFieldđó bị ẩn dưới khung bàn phím. Vì vậy, tôi muốn di chuyển tất cả TextFieldlên tương ứng khi bàn phím đã xuất hiện.

Tôi đã sử dụng mã dưới đây để thêm TextFieldtrên màn hình.

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

Đầu ra:

Đầu ra



1
@PrashantTukadiya Cảm ơn bạn đã phản hồi nhanh chóng. Tôi đã thêm TextField bên trong Scrollview nhưng vẫn gặp phải vấn đề tương tự.
Hitesh Surani

1
@DimaPaliychuk Điều này sẽ không hoạt động. nó là SwiftUI
Prashant Tukadiya

20
Việc hiển thị bàn phím và làm mờ nội dung trên màn hình đã có từ bao giờ, ứng dụng Objective C đầu tiên trên iPhone? Đây là vấn đề liên tục được giải quyết. Tôi thực sự thất vọng vì Apple đã không giải quyết vấn đề này bằng SwiftUi. Tôi biết bình luận này không hữu ích đối với bất kỳ ai, nhưng tôi muốn nêu vấn đề này rằng chúng tôi thực sự nên gây áp lực lên Apple để đưa ra giải pháp chứ không phải dựa vào cộng đồng để luôn cung cấp những vấn đề phổ biến nhất này.
P.

1
Có một bài viết rất hay của Vadim vadimbulavin.com/…
Sudara

Câu trả lời:


64

Đã cập nhật mã cho Xcode, beta 7.

Bạn không cần padding, ScrollViews hoặc Lists để đạt được điều này. Mặc dù giải pháp này cũng sẽ tốt với họ. Tôi bao gồm hai ví dụ ở đây.

Cái đầu tiên di chuyển tất cả textField lên, nếu bàn phím xuất hiện cho bất kỳ cái nào trong số chúng. Nhưng chỉ khi cần thiết. Nếu bàn phím không ẩn các trường văn bản, chúng sẽ không di chuyển.

Trong ví dụ thứ hai, khung nhìn chỉ di chuyển vừa đủ để tránh ẩn trường văn bản đang hoạt động.

Cả hai ví dụ đều sử dụng cùng một mã chung được tìm thấy ở cuối: GeometryGetterKeyboardGuardian

Ví dụ đầu tiên (hiển thị tất cả các trường văn bản)

Khi bàn phím được mở, 3 trường văn bản được di chuyển lên đủ để hiển thị tất cả

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

}

Ví dụ thứ hai (chỉ hiển thị trường đang hoạt động)

Khi mỗi trường văn bản được nhấp, chế độ xem chỉ được di chuyển lên đủ để hiển thị trường văn bản được nhấp.

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

Đây là một khung nhìn hấp thụ kích thước và vị trí của khung nhìn mẹ của nó. Để đạt được điều đó, nó được gọi bên trong công cụ sửa đổi .background. Đây là một công cụ sửa đổi rất mạnh, không chỉ là một cách để trang trí nền của một khung nhìn. Khi chuyển một chế độ xem tới .background (MyView ()), MyView sẽ nhận chế độ xem đã sửa đổi dưới dạng chính. Sử dụng GeometryReader là điều làm cho chế độ xem có thể biết được hình dạng của cha mẹ.

Ví dụ: Text("hello").background(GeometryGetter(rect: $bounds))sẽ lấp đầy các giới hạn thay đổi, với kích thước và vị trí của dạng xem Văn bản và sử dụng không gian tọa độ toàn cục.

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

Cập nhật Tôi đã thêm DispatchQueue.main.async, để tránh khả năng sửa đổi trạng thái của chế độ xem khi nó đang được hiển thị. ***

Bàn phím tiếng Đức

Mục đích của KeyboardGuardian là theo dõi các sự kiện hiển thị / ẩn trên bàn phím và tính toán lượng không gian mà chế độ xem cần được dịch chuyển.

Cập nhật: Tôi đã sửa đổi KeyboardGuardian để làm mới trang trình bày, khi người dùng tab từ trường này sang trường khác

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

        }
    }
}

1
Có thể đính kèm GeometryGetternhư một công cụ sửa đổi chế độ xem thay vì một nền bằng cách làm cho nó tuân theo ViewModifiergiao thức không?
Sudara

2
Có thể, nhưng lợi ích là gì? Bạn sẽ đính kèm nó như thế này: .modifier(GeometryGetter(rect: $kGuardian.rects[1]))thay vì .background(GeometryGetter(rect: $kGuardian.rects[1])). Không có nhiều sự khác biệt (chỉ ít hơn 2 ký tự).
kontiki

3
Trong một số trường hợp, bạn có thể nhận được một KHOẢNG CÁCH TÍN HIỆU từ chương trình bên trong GeometryGetter khi gán hình chữ nhật mới nếu bạn đang điều hướng khỏi màn hình này. Nếu điều đó xảy ra với bạn, bạn chỉ cần thêm một số mã để xác minh rằng kích thước của hình học lớn hơn 0 (Hình học.size.width> 0 && Hình học.size.height> 0) trước khi gán giá trị cho self.rect
Julio Bailon

1
@JulioBailon Tôi không biết tại sao nhưng chuyển geometry.framera khỏi DispatchQueue.main.asynctrợ giúp với SIGNAL ABORT, bây giờ sẽ kiểm tra giải pháp của bạn. Cập nhật: if geometry.size.width > 0 && geometry.size.height > 0trước khi chỉ định self.rectđã trợ giúp.
Roman Vasilyev

2
điều này cũng xảy ra đối với tôi khi self.rect =nticry.frame (trong: .global) nhận được SIGNAL ABORT và đã thử tất cả các giải pháp được đề xuất để giải quyết lỗi này
Marwan Roushdy

55

Để xây dựng giải pháp của @rraphael, tôi đã chuyển đổi nó để có thể sử dụng được nhờ hỗ trợ swiftUI xcode11 ngày nay.

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

Sử dụng:

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

Bản đã xuất bản currentHeightsẽ kích hoạt hiển thị lại giao diện người dùng và di chuyển Trường văn bản của bạn lên khi bàn phím hiển thị và lùi xuống khi bị loại bỏ. Tuy nhiên, tôi không sử dụng ScrollView.


6
Tôi thích câu trả lời này vì sự đơn giản của nó. Tôi đã thêm .animation(.easeOut(duration: 0.16))để cố gắng khớp với tốc độ trượt của bàn phím.
Mark Moeykens

Tại sao bạn đặt chiều cao tối đa là 340 cho bàn phím?
Daniel Ryan

1
@DanielRyan Đôi khi chiều cao bàn phím trả về giá trị không chính xác trong trình mô phỏng. Tôi dường như không thể tìm ra cách để khắc phục sự cố hiện tại
Michael Neas

1
Tôi chưa thấy vấn đề đó bản thân mình. Có thể nó đã được sửa trong các phiên bản mới nhất. Tôi không muốn giảm kích thước trong trường hợp có (hoặc sẽ có) bàn phím lớn hơn.
Daniel Ryan

1
Bạn có thể thử với keyboardFrameEndUserInfoKey. Điều đó sẽ giữ khung cuối cùng cho bàn phím.
Mathias Claassen

50

Tôi đã thử nhiều giải pháp được đề xuất và mặc dù chúng hoạt động trong hầu hết các trường hợp, tôi vẫn gặp một số vấn đề - chủ yếu là với khu vực an toàn (tôi có Biểu mẫu bên trong tab của TabView).

Tôi đã kết thúc việc kết hợp một số giải pháp khác nhau và sử dụng GeometryReader để nhận thông tin chi tiết về vùng dưới cùng khu vực an toàn của chế độ xem cụ thể và sử dụng nó trong tính toán của padding:

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)
                .animation(.easeOut(duration: 0.16))
                .onAppear(perform: {
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillShowNotification)
                        .merge(with: NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillChangeFrameNotification))
                        .compactMap { notification in
                            notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] 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))
                })
        }
    }
}

Sử dụng:

struct MyView: View {
    var body: some View {
        Form {...}
        .modifier(AdaptsToKeyboard())
    }
}

7
Chà, đây là phiên bản SwiftUI nhất trong số chúng, với GeometryReader và ViewModifier. Yêu nó.
Mateusz

3
Điều này rất hữu ích và thanh lịch. Cảm ơn bạn rất nhiều vì đã viết ra điều này.
Danilo Campos

2
Tôi đang nhìn thấy một khung nhìn trắng trống nhỏ trên bàn phím của mình. Chế độ xem này là Chế độ xem GeometryReader, tôi đã xác nhận bằng cách thay đổi màu nền. Bất kỳ ý tưởng nào tại sao GeometryReader lại hiển thị giữa Chế độ xem thực tế và bàn phím của tôi.
user832

6
Tôi gặp lỗi Thread 1: signal SIGABRTtrực tuyến rect.height - geometry.safeAreaInsets.bottomkhi tôi truy cập chế độ xem bằng bàn phím lần thứ hai và nhấp vào TextField. Không quan trọng nếu tôi nhấp vào TextFieldlần đầu tiên hay không. Ứng dụng vẫn bị lỗi.
JLively

2
Cuối cùng một cái gì đó hoạt động! Tại sao Apple không thể làm điều này đối với chúng tôi là đồ dở hơi !!
Dave Kozikowski

35

Tôi đã tạo một Chế độ xem có thể bao bọc bất kỳ chế độ xem nào khác để thu nhỏ nó khi bàn phím xuất hiện.

Nó khá đơn giản. Chúng tôi tạo các nhà xuất bản cho các sự kiện hiển thị / ẩn bàn phím và sau đó đăng ký chúng bằng cách sử dụng onReceive. Chúng tôi sử dụng kết quả đó để tạo một hình chữ nhật có kích thước bằng bàn phím phía sau bàn phím.

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

Sau đó, bạn có thể sử dụng chế độ xem như sau:

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

Để di chuyển nội dung của dạng xem lên thay vì thu nhỏ nó, có thể thêm phần đệm hoặc phần bù vào viewthay vì đặt nó vào VStack có hình chữ nhật.


6
Tôi nghĩ đây là câu trả lời đúng. Chỉ là một chỉnh sửa nhỏ mà tôi đã làm: thay vì một hình chữ nhật, tôi chỉ sửa đổi phần đệm self.viewvà nó hoạt động rất tốt. Không có vấn đề gì với hoạt ảnh
Tae

5
Cảm ơn! Hoạt động hoàn hảo. Như @Taed đã nói, tốt hơn là sử dụng phương pháp đệm. Kết quả cuối cùng sẽ làvar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
Mặc dù số phiếu bầu ít hơn, đây là câu trả lời nhanh chóng nhất. Và cách tiếp cận trước đây bằng cách sử dụng AnyView, phá vỡ trợ giúp tăng tốc Metal.
Nelson Cardaci

4
Đó là một giải pháp tuyệt vời, nhưng vấn đề chính ở đây là bạn mất khả năng di chuyển lên trên chế độ xem chỉ khi bàn phím đang ẩn trường văn bản bạn đang chỉnh sửa. Ý tôi là: nếu bạn có một biểu mẫu với một số trường văn bản và bạn bắt đầu chỉnh sửa trường đầu tiên ở trên cùng, bạn có thể không muốn nó di chuyển lên vì nó sẽ di chuyển ra khỏi màn hình.
superpuccio

Tôi thực sự thích câu trả lời, nhưng giống như tất cả các câu trả lời khác, nó không hoạt động nếu chế độ xem của bạn nằm bên trong TabBar hoặc Chế độ xem không bằng phẳng với cuối màn hình.
Ben Patch

25

Tôi đã tạo một công cụ sửa đổi chế độ xem thực sự đơn giản.

Thêm tệp Swift với mã bên dưới và chỉ cần thêm công cụ sửa đổi này vào chế độ xem của bạn:

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


Đẹp. Cám ơn vì đã chia sẻ.
thập kỷ

2
Sẽ rất tuyệt, nếu nó chỉ bù đắp, nếu cần (tức là không cuộn, nếu bàn phím không che phần tử đầu vào). Rất vui khi có ...
nhiều thập kỷ

Điều này làm việc tuyệt vời, cảm ơn bạn. Việc triển khai rất sạch sẽ và đối với tôi, chỉ cuộn nếu cần.
Joshua

Tuyệt vời! Tại sao bạn không cung cấp điều này trên Github hoặc nơi khác? :) Hoặc bạn có thể đề xuất điều này cho github.com/hackiftekhar/IQKeyboardManager vì họ chưa có hỗ trợ SwiftUI đầy đủ
Schnodderbalken

Sẽ không tốt với những thay đổi về hướng và sẽ bù đắp bất kể nó có cần thiết hay không.
GrandSteph

16

Hoặc bạn chỉ có thể sử dụng IQKeyBoardManagerSwift

và có thể tùy chọn thêm điều này vào đại biểu ứng dụng của bạn để ẩn thanh công cụ và cho phép ẩn bàn phím khi nhấp vào bất kỳ chế độ xem nào khác sau đó là bàn phím.

        IQKeyboardManager.shared.enableAutoToolbar = false
        IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide

Đây quả thực là cách (bất ngờ) đối với tôi. Chất rắn.
HelloTimo

Khung này thậm chí còn hoạt động tốt hơn mong đợi. Cảm ơn bạn đã chia sẻ!
Richard Poutier

1
Tôi làm việc ổn trên SwiftUI - cảm ơn @DominatorVbN - Tôi ở chế độ ngang iPad, tôi cần tăng IQKeyboardManager.shared.keyboardDistanceFromTextFieldlên 40 để có được khoảng trống thoải mái.
Richard Groves

Cũng phải đặt IQKeyboardManager.shared.enable = trueđể giữ cho bàn phím không ẩn các trường văn bản của tôi. Trong mọi trường hợp đây là giải pháp tốt nhất. Tôi có 4 trường được sắp xếp theo chiều dọc và các giải pháp khác sẽ hoạt động cho trường dưới cùng của tôi, nhưng sẽ đẩy trường trên cùng ra khỏi chế độ xem.
MisterEd

12

Bạn cần thêm một ScrollViewvà đặt một phần đệm dưới cùng bằng kích thước của bàn phím để nội dung có thể cuộn khi bàn phím xuất hiện.

Để có được kích thước bàn phím, bạn sẽ cần sử dụng NotificationCenterđể đăng ký sự kiện bàn phím. Bạn có thể sử dụng một lớp tùy chỉnh để làm như vậy:

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

Sự BindableObjecttuân thủ sẽ cho phép bạn sử dụng lớp này như một Statevà kích hoạt cập nhật chế độ xem. Nếu cần, hãy xem hướng dẫn cho BindableObject: Hướng dẫn SwiftUI

Khi bạn nhận được điều đó, bạn cần định cấu hình a ScrollViewđể giảm kích thước của nó khi bàn phím xuất hiện. Để thuận tiện, tôi đã gói nó ScrollViewthành một số loại thành phần:

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

Tất cả những gì bạn phải làm bây giờ là nhúng nội dung của bạn vào bên trong tùy chỉnh 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)
    }
}

Chỉnh sửa: Hành vi cuộn thực sự kỳ lạ khi bàn phím đang ẩn. Có thể việc sử dụng hoạt ảnh để cập nhật phần đệm sẽ khắc phục được điều này hoặc bạn nên cân nhắc sử dụng thứ gì đó khác hơn là paddingđể điều chỉnh kích thước chế độ xem cuộn.


này, có vẻ như bạn có kinh nghiệm về bindableobject. Tôi không thể làm cho nó hoạt động như tôi muốn. Nó sẽ được tốt đẹp nếu bạn có thể nhìn qua: stackoverflow.com/questions/56500147/...
SwiftiSwift

Tại sao bạn không sử dụng @ObjectBinding
SwiftiSwift

3
Với BindableObjectphản đối, điều này không hoạt động nữa, không may.
LinusGeffarth

2
@LinusGeffarth Đối với những gì nó đáng giá, BindableObjectvừa được đổi tên thành ObservableObjectdidChangethành objectWillChange. Đối tượng cập nhật chế độ xem tốt (mặc dù tôi đã thử nghiệm bằng cách sử dụng @ObservedObjectthay vì @State)
SeizeTheDay 23/10 '19

Xin chào, giải pháp này đang cuộn nội dung, nhưng nó hiển thị một số khu vực màu trắng phía trên bàn phím ẩn một nửa trường văn bản. Vui lòng cho tôi biết cách chúng tôi có thể loại bỏ vùng màu trắng.
Shahbaz Sajjad

12

Xcode 12 - Mã một dòng

Thêm công cụ sửa đổi này vào TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Bản giới thiệu

Apple đã thêm bàn phím làm khu vực cho khu vực an toàn, vì vậy bạn có thể sử dụng bàn phím để di chuyển bất kỳView bằng bàn phím như các khu vực khác.


nó hoạt động cho TextField, nhưng còn TextEditor thì sao?
Андрей Первушин

Nó hoạt động trên Bất kỳ View , bao gồm cả TextEditor.
Mojtaba Hosseini

3
vừa được thử nghiệm, Xcode beta 6, TextEditor không hoạt động
Андрей Первушин

1
Bạn có thể đặt một câu hỏi và liên kết nó ở đây. Vì vậy, tôi có thể xem mã có thể sao chép của bạn và xem liệu tôi có thể giúp gì không :) @kyrers
Mojtaba Hosseini

2
Tôi đã tự mình tìm ra. Thêm .ignoresSafeArea(.keyboard)vào Chế độ xem của bạn.
leonboe1

6

Tôi đã xem xét và cấu trúc lại các giải pháp hiện có thành một gói SPM tiện dụng cung cấp công cụ .keyboardAware()sửa đổi:

Bàn phímAwareSwiftUI

Thí dụ:

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

    }
}

Nguồn:

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

Tôi chỉ thấy một nửa chiều cao của textview. bạn có biết làm thế nào để giải quyết điều này?
Matrosov Alexander

Tốt. điều này đã tiết kiệm thời gian của tôi. Trước khi sử dụng, chúng ta biết phần đệm dưới cùng của chế độ xem công cụ sửa đổi này.
Brownsoo Han

5

Tôi đã sử dụng câu trả lời của Benjamin Kindle làm điểm khởi đầu, nhưng tôi có một số vấn đề muốn giải quyết.

  1. Hầu hết các câu trả lời ở đây không liên quan đến việc bàn phím thay đổi khung của nó, vì vậy chúng sẽ bị vỡ nếu người dùng xoay thiết bị với bàn phím trên màn hình. Thêm keyboardWillChangeFrameNotificationvào danh sách các thông báo đã xử lý địa chỉ này.
  2. Tôi không muốn nhiều nhà xuất bản có bản đồ đóng cửa giống nhau nhưng khác nhau, vì vậy tôi đã xâu chuỗi cả ba thông báo bàn phím vào một nhà xuất bản duy nhất. Nó được thừa nhận là một chuỗi dài nhưng mỗi bước khá đơn giản.
  3. Tôi đã cung cấp inithàm chấp nhận a @ViewBuilderđể bạn có thể sử dụng KeyboardHostchế độ xem giống như bất kỳ Chế độ xem nào khác và chỉ cần chuyển nội dung của bạn trong một phần đóng cuối, trái ngược với việc chuyển chế độ xem nội dung làm tham số init.
  4. Như Tae và fdelafuente đề nghị trong phần bình luận, tôi đã đổi chỗ Rectangleđể điều chỉnh phần đệm phía dưới.
  5. Thay vì sử dụng chuỗi "UIKeyboardFrameEndUserInfoKey" được mã hóa cứng, tôi muốn sử dụng các chuỗi được cung cấp UIWindowdưới dạng UIWindow.keyboardFrameEndUserInfoKey.

Kết hợp tất cả lại với nhau tôi có:

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


giải pháp này không hoạt động, nó làm tăng Keyboardchiều cao
GSerjo

Bạn có thể giải thích thêm về các vấn đề mà bạn đang gặp @GSerjo không? Tôi đang sử dụng mã này trong ứng dụng của mình và nó hoạt động tốt đối với tôi.
Timothy Sanders

Bạn có thể vui lòng bật Pridictivetrong iOS keyboard. Settings-> General-> Keyboard-> Pridictive. trong trường hợp này, nó không sửa được tấm đệm và thêm phần đệm vào bàn phím
GSerjo 23/09/19

@GSerjo: Tôi đã bật văn bản Dự đoán trên iPad Touch (thế hệ thứ 7) chạy iOS 13.1 beta. Nó bổ sung chính xác khoảng đệm cho chiều cao của hàng dự đoán. (Điều quan trọng cần lưu ý, tôi không điều chỉnh độ cao của bàn phím ở đây, tôi đang thêm vào phần đệm của chính chế độ xem.) Hãy thử hoán đổi trong bản đồ gỡ lỗi được nhận xét và chơi với các giá trị bạn nhận được cho dự đoán bàn phím. Tôi sẽ đăng nhật ký trong một bình luận khác.
Timothy Sanders

Với bản đồ "gỡ lỗi" bỏ ghi chú, bạn có thể thấy giá trị được gán cho keyboardHeight. Trên iPod Touch của tôi (ở chế độ dọc), bàn phím có bật dự đoán là 254 điểm. Nếu không có nó là 216 điểm. Tôi thậm chí có thể tắt tính năng tiên đoán bằng màn hình bàn phím và cập nhật phần đệm đúng cách. Thêm bàn phím có tính năng tiên đoán: Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 Khi tôi tắt văn bản tiên đoán:Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders

4

Điều này được phỏng theo những gì @kontiki đã xây dựng. Tôi có nó đang chạy trong một ứng dụng dưới dạng hạt giống beta 8 / GM, trong đó trường cần cuộn là một phần của biểu mẫu bên trong NavigationView. Đây là Bàn phímGuardian:

//
//  KeyboardGuardian.swift
//
//  /programming/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
        }
    }
}

Sau đó, tôi sử dụng một enum để theo dõi các vị trí trong mảng rects và tổng số:

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

KeyboardSlots.count.rawValuelà dung lượng mảng cần thiết; những cái khác dưới dạng rawValue cung cấp chỉ mục thích hợp mà bạn sẽ sử dụng cho các lệnh gọi .background (GeometryGetter).

Với thiết lập đó, lượt xem nhận được tại KeyboardGuardian với điều này:

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

Chuyển động thực tế là như thế này:

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

gắn vào khung nhìn. Trong trường hợp của tôi, nó được gắn vào toàn bộ NavigationView, do đó, cụm hoàn chỉnh sẽ trượt lên khi bàn phím xuất hiện.

Tôi chưa giải quyết được vấn đề nhận được thanh công cụ Xong hoặc phím quay lại trên bàn phím thập phân với SwiftUI, vì vậy thay vào đó tôi đang sử dụng điều này để ẩn nó khi chạm ở một nơi khác:

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

Bạn đính kèm nó vào một dạng xem như

.modifier(DismissingKeyboard())

Một số chế độ xem (ví dụ: bộ chọn) không thích gắn nó vào, vì vậy bạn có thể cần phải chi tiết hóa cách bạn đính kèm công cụ sửa đổi thay vì chỉ vỗ nó vào chế độ xem ngoài cùng.

Rất cám ơn @kontiki vì đã làm việc chăm chỉ. Bạn sẽ vẫn cần GeometryGetter của anh ấy ở trên (không, tôi cũng không thực hiện công việc chuyển đổi nó để sử dụng các tùy chọn) như anh ấy minh họa trong các ví dụ của mình.


1
Đối với cá nhân đã phản đối: tại sao? Tôi đã cố gắng thêm một cái gì đó hữu ích, vì vậy tôi muốn biết theo quan điểm của bạn, tôi đã sai như thế nào
Feldur 27/09/19

4

Một số giải pháp ở trên có một số vấn đề và không nhất thiết phải là cách tiếp cận "sạch sẽ nhất". Bởi vì điều này, tôi đã sửa đổi một số điều để triển khai bên dưới.

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

Tôi sẽ thích một cách tiếp cận rõ ràng hơn và chuyển trách nhiệm sang chế độ xem đã xây dựng (không phải công cụ sửa đổi) trong cách bù đắp nội dung, nhưng có vẻ như tôi không thể khiến nhà xuất bản kích hoạt đúng cách khi di chuyển mã bù sang chế độ xem. ...

Cũng lưu ý rằng Publishers phải được sử dụng trong trường hợp này vì final classhiện tại gây ra lỗi ngoại lệ không xác định (mặc dù nó đáp ứng các yêu cầu về giao diện) và ScrollView tổng thể là cách tiếp cận tốt nhất khi áp dụng mã bù đắp.


Giải pháp rất tốt, rất khuyến khích! Tôi đã thêm Bool để cho biết bàn phím hiện đang hoạt động hay không.
Peanutsmasher

4

Thành thật mà nói, rất nhiều câu trả lời có vẻ thực sự cồng kềnh. Nếu bạn đang sử dụng SwiftUI thì bạn cũng có thể sử dụng Kết hợp.

Tạo một KeyboardRespondernhư hình dưới đây, sau đó bạn có thể sử dụng như đã trình bày trước đó.

Đã cập nhật cho 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)
    }
}

Tôi đã thêm .animation (.easeIn) để phù hợp với hoạt ảnh mà bàn phím xuất hiện
Michiel Pelt

(Đối với iOS 13, hãy truy cập lịch sử của câu trả lời này)
Loris Foe

Xin chào, .assign (to: \ .keyboardHeight) đang đưa ra lỗi "Không thể suy ra loại đường dẫn khóa từ ngữ cảnh; hãy xem xét chỉ định rõ ràng loại gốc". Vui lòng cho tôi biết giải pháp thích hợp và sạch sẽ cho cả ios 13 và ios 14.
Shahbaz Sajjad

3

Tôi không chắc chắn nếu API chuyển / hoạt hình cho SwiftUI hoàn tất, nhưng bạn có thể sử dụng CGAffineTransformvới.transformEffect

Tạo một đối tượng bàn phím có thể quan sát với thuộc tính đã xuất bản như sau:

    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
    }

}

thì bạn có thể sử dụng thuộc tính đó để sắp xếp lại chế độ xem của mình như sau:

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

hoặc đơn giản hơn

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

Tôi thích câu trả lời này, nhưng tôi không chắc 'ScreenSize.portrait' đến từ đâu.
Misha Stone

Xin chào @MishaStone, cảm ơn vì đã chọn cách tiếp cận của tôi. ScreenSize.portrait là một lớp mà tôi thực hiện để có được số đo của cơ sở trên màn hình định hướng và tỷ lệ phần trăm .... nhưng bạn có thể thay thế bằng bất kỳ giá trị mà bạn cần cho bản dịch của mình
blacktiago

3

Xcode 12 beta 4 thêm một công cụ sửa đổi chế độ xem mới ignoresSafeAreamà bạn hiện có thể sử dụng để tránh bàn phím.

.ignoresSafeArea([], edges: [])

Điều này tránh bàn phím và tất cả các cạnh khu vực an toàn. Bạn có thể đặt tham số đầu tiên thành .keyboardnếu bạn không muốn nó bị tránh. Có một số điều kỳ quặc đối với nó, ít nhất là trong thiết lập phân cấp chế độ xem của tôi, nhưng có vẻ như đây là cách Apple muốn chúng ta tránh bàn phím.


2

Câu trả lời được sao chép từ đây: TextField luôn ở trên cùng bàn phím với SwiftUI

Tôi đã thử các cách tiếp cận khác nhau và không có cách nào trong số đó hiệu quả với tôi. Cái này bên dưới là cái duy nhất hoạt động cho các thiết bị khác nhau.

Thêm phần mở rộng này vào một tệp:

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

Theo quan điểm của bạn, bạn cần một biến để ràng buộc offsetValue:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

  var body: some View { 
    
    VStack {
     //...       
    }
    .keyboardSensible($offsetValue)
  }
}

2
Chỉ cần một FYI, bạn sở hữu các đối tượng khi gọi NotificationCenter.default.addObserver... bạn cần lưu trữ chúng và loại bỏ các quan sát viên vào thời điểm thích hợp ...
TheCodingArt

Xin chào @TheCodingArt, đúng vậy, tôi đã cố gắng làm điều đó như thế này ( oleb.net/blog/2018/01/notificationcenter-removeobserver ) nhưng nó dường như không hiệu quả với tôi, có ý kiến ​​gì không?
Ray

2

Như Mark Krenek và Heiko đã chỉ ra, Apple dường như đã giải quyết vấn đề này từ lâu trong Xcode 12 beta 4. Mọi thứ đang tiến triển nhanh chóng. Theo ghi chú phát hành cho Xcode 12 beta 5 được xuất bản ngày 18 tháng 8 năm 2020 "Biểu mẫu, Danh sách và TextEditor không còn ẩn nội dung đằng sau bàn phím. (66172025)". Tôi chỉ cần tải xuống và thử nghiệm nhanh trong trình mô phỏng beta 5 (iPhone SE2) với vùng chứa Biểu mẫu trong ứng dụng mà tôi đã khởi chạy cách đây vài ngày.

Nó bây giờ "chỉ hoạt động" cho một TextField . SwiftUI sẽ tự động cung cấp phần đệm dưới cùng thích hợp cho Biểu mẫu đóng gói để nhường chỗ cho bàn phím. Và nó sẽ tự động cuộn Biểu mẫu lên để hiển thị Trường văn bản ngay phía trên bàn phím. Vùng chứa ScrollView hiện hoạt động tốt khi bàn phím xuất hiện.

Tuy nhiên, như Андрей Первушин đã chỉ ra trong một nhận xét, có một vấn đề với TextEditor . Beta 5 & 6 sẽ tự động cung cấp phần đệm dưới cùng thích hợp cho Biểu mẫu đóng gói để nhường chỗ cho bàn phím. Nhưng nó sẽ KHÔNG tự động cuộn Biểu mẫu lên. Bàn phím sẽ bao phủ TextEditor. Vì vậy, không giống như TextField, người dùng phải cuộn Form để hiển thị TextEditor. Tôi sẽ gửi một báo cáo lỗi. Có lẽ Beta 7 sẽ sửa chữa nó. Thật gần…

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


tôi thấy ghi chú phát hành của apple, được thử nghiệm trên beta5 và beta6, TextField hoạt động, TextEditor KHÔNG, tôi bỏ lỡ điều gì? @State var text = "" var body: some View {Form {Section {Text (text) .frame (height: 500)} Section {TextField ("5555", text: $ text) .frame (height: 50)} Phần {TextEditor (text: $ text) .frame (height: 120)}}}
Андрей Первушин

2

Sử dụng:

import SwiftUI

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

Mã:

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
            .filter({$0.activationState == .foregroundActive})
            .map({$0 as? UIWindowScene})
            .compactMap({$0})
            .first?.windows
            .filter({$0.isKeyWindow}).first
        return keyWindow
    }
}

đã thử giải pháp của bạn ... chế độ xem chỉ được cuộn đến một nửa trường văn bản. Đã thử tất cả các giải pháp trên. Có cùng một vấn đề. Xin vui lòng giúp đỡ!!!
Sona

@Zeona, hãy thử trong một ứng dụng đơn giản, bạn có thể đang làm điều gì đó khác biệt. Ngoài ra, hãy thử xóa '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)' nếu bạn đang sử dụng khu vực an toàn.
8suhas

đã xóa (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0) thì tôi nhận được khoảng trắng phía trên bàn phím
Sona

1

Xử lý TabView's

Tôi thích câu trả lời của Benjamin Kindle nhưng nó không hỗ trợ TabViews. Đây là điều chỉnh của tôi đối với mã của anh ấy để xử lý TabViews:

  1. Thêm phần mở rộng để UITabViewlưu trữ kích thước của tabView khi khung của nó được đặt. Chúng tôi có thể lưu trữ điều này trong một biến tĩnh vì thường chỉ có một tabView trong một dự án (nếu dự án của bạn có nhiều hơn một, thì bạn sẽ cần phải điều chỉnh).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. Bạn sẽ cần phải thay đổi nó onReceiveở cuối KeyboardHostchế độ xem để tính đến chiều cao của Thanh Tab:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. Và đó là nó! Siêu đơn giản 🎉.

1

Tôi đã thực hiện một cách tiếp cận hoàn toàn khác, bằng cách mở rộng UIHostingControllervà điều chỉnh 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()
    }
}

Sau đó đổi SceneDelegatesang sử dụng MyHostingControllerthay thế UIHostingController.

Khi hoàn tất, tôi không cần phải lo lắng về bàn phím bên trong mã SwiftUI của mình.

(Lưu ý: Tôi chưa sử dụng đủ điều này để hiểu đầy đủ về bất kỳ hàm ý nào của việc làm này!)


1

Đây là cách tôi xử lý bàn phím trong SwiftUI. Điều cần nhớ là nó đang thực hiện các phép tính trên VStack mà nó được gắn vào.

Bạn sử dụng nó trên Chế độ xem dưới dạng Bổ trợ. Cách này:

struct LogInView: View {

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

Vì vậy, để đến với công cụ sửa đổi này, trước tiên, hãy tạo một phần mở rộng của UIResponder để có được vị trí TextField đã chọn trong 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)
  }
}

Bây giờ bạn có thể tạo KeyboardModifier bằng Kết hợp để tránh bàn phím ẩn Trường văn bản:

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

1

Đối với iOS 14 (beta 4), nó hoạt động khá đơn giản:

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

Và kích thước của chế độ xem sẽ điều chỉnh theo đầu bàn phím. Chắc chắn có nhiều tinh chỉnh hơn có thể với khung hình (.maxHeight: ...), v.v. Bạn sẽ tìm ra điều đó.

Thật không may, bàn phím nổi trên iPad vẫn gây ra sự cố khi di chuyển. Nhưng các giải pháp được đề cập ở trên cũng sẽ xảy ra và nó vẫn đang ở giai đoạn thử nghiệm, tôi hy vọng họ sẽ tìm ra.

Thx Apple, cuối cùng!


Điều này hoàn toàn không hoạt động (14.1). Ý tưởng là gì?
Burgler-dev

0

Chế độ xem của tôi:

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

VM của tôi:

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

Về cơ bản, Quản lý phần đệm dưới cùng dựa trên chiều cao bàn phím.


-3

Câu trả lời thanh lịch nhất mà tôi đã quản lý cho điều này tương tự như giải pháp của rraphael. Tạo một lớp để lắng nghe các sự kiện bàn phím. Tuy nhiên, thay vì sử dụng kích thước bàn phím để sửa đổi phần đệm, hãy trả về giá trị âm của kích thước bàn phím và sử dụng công cụ sửa đổi .offset (y :) để điều chỉnh độ lệch của vùng chứa bên ngoài chế độ xem nhất. Nó hoạt hình đủ tốt và hoạt động với mọi chế độ xem.


Làm thế nào bạn có được điều này để hoạt hình? Tôi có .offset(y: withAnimation { -keyboard.currentHeight }), nhưng nội dung nhảy thay vì hoạt ảnh.
jjatie 29/07/19

Cách đây một vài lần tôi đã thử nghiệm với đoạn mã này, nhưng tại thời điểm nhận xét trước đó của tôi, việc sửa đổi độ lệch của vstack trong thời gian chạy là tất cả những gì được yêu cầu, SwiftUI sẽ tạo hiệu ứng thay đổi cho bạn.
pcallycat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.