SwiftUI에 키보드가 나타나면 TextField를 위로 이동


100

TextField내 메인 안에 7 개가 있습니다 ContentView. 사용자가 키보드를 열면 일부가 TextField키보드 프레임 아래에 숨겨집니다. 그래서 TextField키보드가 나타나면 모두 위로 이동하고 싶습니다 .

아래 코드를 사용 TextField하여 화면 에 추가 했습니다.

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

산출:

산출



1
@PrashantTukadiya 빠른 응답에 감사드립니다. Scrollview 내부에 TextField를 추가했지만 여전히 동일한 문제가 발생합니다.
Hitesh Surani

1
@DimaPaliychuk 이것은 작동하지 않습니다. 그것은 SwiftUI입니다
Prashant Tukadiya

28
첫 번째 Objective C iPhone 앱이 출시 된 이후로 키보드와 화면의 내용이 가려지는 문제가있었습니다. 이것은 끊임없이 해결되고있는 문제입니다. Apple이 SwiftUi로이 문제를 해결하지 않은 것에 실망합니다. 나는이 의견이 누구에게도 도움이되지 않는다는 것을 알고 있지만,이 문제를 제기하고 싶었습니다. 우리는 Apple이 해결책을 제공하도록 압력을 가하고 항상 가장 일반적인 문제를 제공하기 위해 커뮤니티에 의존하지 말아야합니다.
P. Ent 2019

3
바딤에 의해 매우 좋은 기사가 vadimbulavin.com/...
Sudara

답변:


64

Xcode 베타 7 용 코드가 업데이트되었습니다.

이를 위해 패딩, ScrollViews 또는 목록이 필요하지 않습니다. 이 솔루션은 그들과도 잘 어울릴 것입니다. 여기에 두 가지 예가 포함되어 있습니다.

첫 번째 는 키보드가 나타나면 모든 textField를 위로 이동 합니다. 그러나 필요한 경우에만. 키보드가 텍스트 필드를 숨기지 않으면 움직이지 않습니다.

두 번째 예에서보기는 활성 텍스트 필드를 숨기지 않도록 충분히 이동합니다.

두 예제 모두 끝에있는 동일한 공통 코드 인 GeometryGetterKeyboardGuardian을 사용합니다.

첫 번째 예 (모든 텍스트 필드 표시)

키보드가 열리면 3 개의 텍스트 필드가 위로 이동하여 모두 표시됩니다.

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

}

두 번째 예 (활성 필드 만 표시)

각 텍스트 필드를 클릭하면 클릭 한 텍스트 필드를 볼 수있을만큼만보기가 위로 이동합니다.

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

}

기하학 Getter

이것은 상위 뷰의 크기와 위치를 흡수하는 뷰입니다. 이를 달성하기 위해 .background 수정 자 내부에서 호출됩니다. 이것은 단순히 뷰의 배경을 장식하는 방법이 아니라 매우 강력한 수정 자입니다. .background (MyView ())에 뷰를 전달할 때 MyView는 수정 된 뷰를 부모로 가져옵니다. GeometryReader를 사용하면 뷰가 부모의 지오메트리를 알 수 있습니다.

예 : Text("hello").background(GeometryGetter(rect: $bounds))텍스트보기의 크기와 위치 및 전역 좌표 공간을 사용하여 변수 경계를 채 웁니다.

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

업데이트 DispatchQueue.main.async를 추가하여 렌더링되는 동안 뷰의 상태를 수정할 가능성을 방지했습니다. ***

KeyboardGuardian

KeyboardGuardian의 목적은 키보드 표시 / 숨기기 이벤트를 추적하고보기를 이동해야하는 공간의 양을 계산하는 것입니다.

업데이트 : 사용자가 한 필드에서 다른 필드로 탭할 때 슬라이드를 새로 고치도록 KeyboardGuardian을 수정했습니다 .

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
프로토콜을 GeometryGetter준수하여 배경이 아닌 뷰 수정 자로 첨부 할 수 ViewModifier있습니까?
Sudara 19

2
가능하지만 이득은 무엇입니까? .modifier(GeometryGetter(rect: $kGuardian.rects[1]))대신 다음과 같이 첨부합니다 .background(GeometryGetter(rect: $kGuardian.rects[1])). 그다지 차이가 ​​없습니다 (2 자 미만).
kontiki

3
일부 상황에서는이 화면에서 벗어나 탐색하는 경우 새 사각형을 할당 할 때 GeometryGetter 내부의 프로그램에서 SIGNAL ABORT를 얻을 수 있습니다. 이 경우 self.rect에 값을 할당하기 전에 도형의 크기가 0 (geometry.size.width> 0 && geometry.size.height> 0)보다 큰지 확인하는 코드를 추가하기 만하면됩니다.
Julio Bailon

1
@JulioBailon 왜 그런지 모르겠지만 SIGNAL ABORT geometry.frameDispatchQueue.main.async도움을 받으면 이제 솔루션을 테스트합니다. 업데이트 : 도움 if geometry.size.width > 0 && geometry.size.height > 0을 할당하기 전에 self.rect.
Roman Vasilyev

2
self.rect = geometry.frame (에 : .global)에뿐만 아니라 나를 위해이 중단 신호 ABORT를 받고이 오류를 해결하기 위해 제안 된 모든 솔루션을 시도
마르 완 Roushdy

55

@rraphael의 솔루션을 구축하기 위해 오늘의 xcode11 swiftUI 지원에서 사용할 수 있도록 변환했습니다.

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

용법:

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

게시 된 항목 currentHeight은 UI 다시 렌더링을 트리거하고 키보드가 표시되면 TextField를 위로 이동하고 닫으면 다시 아래로 이동합니다. 그러나 ScrollView를 사용하지 않았습니다.


6
나는 단순함 때문에이 대답을 좋아합니다. 나는 .animation(.easeOut(duration: 0.16))키보드가 미끄러지는 속도를 맞추기 위해 추가 했습니다.
Mark Moeykens

키보드의 최대 높이를 340으로 설정 한 이유는 무엇입니까?
Daniel Ryan

1
@DanielRyan 때때로 키보드 높이가 시뮬레이터에서 잘못된 값을 반환했습니다. 나는 현재 문제를 핀 수있는 방법 알아낼 수 없습니다
마이클 NEAS에게

1
나는 그 문제를 직접 보지 못했습니다. 최신 버전에서 수정되었을 수 있습니다. 더 큰 키보드가 있거나있을 경우를 대비하여 크기를 잠그고 싶지 않았습니다.
Daniel Ryan

1
으로 시도 할 수 있습니다 keyboardFrameEndUserInfoKey. 키보드의 마지막 프레임을 유지해야합니다.
Mathias Claassen

51

제안 된 솔루션을 많이 시도해 보았지만 대부분의 경우 작동하지만 주로 안전 영역에 문제가있었습니다 (TabView의 탭 안에 Form이 있습니다).

나는 몇 가지 다른 솔루션을 결합하고 GeometryReader를 사용하여 특정 뷰의 안전 영역 하단 삽입을 얻고 패딩 계산에 사용했습니다.

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

용법:

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

7
와우, 이것은 GeometryReader 및 ViewModifier를 사용하는 가장 SwiftUI 버전입니다. 그것을 사랑하십시오.
Mateusz

3
이것은 매우 유용하고 우아합니다. 이 글을 써 주셔서 감사합니다.
Danilo Campos

2
키보드 위에 작은 빈 흰색보기가 표시됩니다. 이보기는 GeometryReader보기이며 배경색을 변경하여 확인했습니다. GeometryReader가 실제보기와 키보드 사이에 표시되는 이유를 알 수 있습니다.
user832

6
나는 오류 받고 있어요 Thread 1: signal SIGABRT라인을 rect.height - geometry.safeAreaInsets.bottom나는 키보드로보기에 두 번 가서를 클릭 할 때 TextField. TextField처음 클릭하든 아니든 상관 없습니다. 앱이 여전히 충돌합니다.
JLively

2
마침내 작동하는 것! 애플이 우리를 위해 이것을 할 수없는 이유는 미쳤다 !!
Dave Kozikowski

35

다른 뷰를 래핑하여 키보드가 나타날 때 축소 할 수있는 뷰를 만들었습니다.

매우 간단합니다. 키보드 표시 / 숨기기 이벤트에 대한 게시자를 만든 다음을 사용하여 구독합니다 onReceive. 그 결과를 사용하여 키보드 뒤에 키보드 크기의 직사각형을 만듭니다.

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

그런 다음 다음과 같이보기를 사용할 수 있습니다.

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

뷰의 내용을 축소하지 않고 위로 이동하려면 view사각형이있는 VStack에 넣는 대신 패딩 또는 오프셋을 추가 할 수 있습니다 .


6
이것이 정답이라고 생각합니다. 내가 한 사소한 조정 : 직사각형 대신 패딩을 수정 self.view하고 있으며 훌륭하게 작동합니다. 애니메이션에 전혀 문제가 없습니다
Tae

5
감사! 완벽하게 작동합니다. @Taed가 말했듯이 패딩 접근 방식을 사용하는 것이 좋습니다. 최종 결과는 다음과 같습니다var body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente 2019-08-09

1
적은 투표에도 불구하고 이것은 가장 빠른 응답입니다. 그리고 AnyView를 사용한 이전 접근 방식은 Metal 가속 도움말을 깨뜨립니다.
Nelson Cardaci

4
훌륭한 솔루션이지만 여기서 주요 문제는 키보드가 편집중인 텍스트 필드를 숨기는 경우에만보기를 위로 이동할 수있는 기능이 없다는 것입니다. 내 말은 : 여러 개의 텍스트 필드가있는 양식이 있고 맨 위에있는 첫 번째 항목을 편집하기 시작하면 화면 밖으로 이동하므로 위로 이동하는 것을 원하지 않을 것입니다.
matteopuc

나는 대답이 정말 마음에 들지만 다른 모든 대답과 마찬가지로 뷰가 TabBar 내부에 있거나 뷰가 화면 하단과 같지 않으면 작동하지 않습니다.
Ben 패치

25

보기 수정자를 사용하기 정말 간단합니다.

아래 코드로 Swift 파일을 추가하고이 수정자를 뷰에 추가하기 만하면됩니다.

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


2
필요한 경우에만 오프셋이 적용된다면 멋질 것입니다 (예 : 키보드가 입력 요소를 덮지 않는 경우 스크롤하지 않음). 가지고있어서 좋은 ...
수십 년

잘 작동합니다. 감사합니다. 매우 깨끗한 구현도 필요하며 필요한 경우에만 스크롤됩니다.
Joshua

대박! Github 또는 다른 곳에서 제공하지 않는 이유는 무엇입니까? : 또는 당신은이 제안 할 수 github.com/hackiftekhar/IQKeyboardManager 가 아직 전체 SwiftUI를 지원하지 않는
Schnodderbalken

방향 변경으로 잘 작동하지 않으며 필요 여부에 관계없이 오프셋됩니다.
GrandSteph

여기에서 한 가지 문제는 이것이 전혀 애니메이션되지 않는다는 것입니다 ... 매우 불안한 ​​모션을 생성합니다 😢
Zorayr

22

Xcode 12-한 줄 코드

이 수정자를 TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

데모

Apple은 키보드를 안전 영역에 대한 영역으로 추가 했으므로 다른 영역과 마찬가지로 키보드로 키보드 를 이동 하는 데View 사용할 수 있습니다 .


을 포함하여 Any View 에서 작동 합니다 TextEditor.
Hosseini

@MojtabaHosseini이 동작을 방지하려면 어떻게해야합니까? 이제 키보드를 열 때 위로 이동하는 이미지가 있는데 이동하고 싶지 않습니다.
kyrers

1
여기에서 질문하고 연결할 수 있습니다. 난 당신 재현 할 코드를 살펴보고 볼 수 있도록 내가 도울 수 있다면 :) @kyrers을
모즈 타바 호세 이니

2
나는 그것을 스스로 알아 냈다. .ignoresSafeArea(.keyboard)보기에 추가 하십시오.
leonboe1

3
불행하게도 아이폰 OS (14)의 만 ...
Xaxxus

16

또는 IQKeyBoardManagerSwift를 사용할 수 있습니다.

도구 모음을 숨기고 키보드가 아닌 다른보기를 클릭 할 때 키보드를 숨기도록 선택적으로 앱 델리게이트에 추가 할 수 있습니다.

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

이것은 실제로 나에게도 (예기치 않은) 방법입니다. 고체.
HelloTimo

이 프레임 워크는 예상보다 훨씬 더 잘 작동했습니다. 공유 해주셔서 감사합니다!
Richard Poutier

1
SwiftUI에서 잘 작동합니다. @DominatorVbN 감사합니다. iPad 가로 모드에서 IQKeyboardManager.shared.keyboardDistanceFromTextField편안한 간격을 확보하려면 40으로 늘려야 했습니다.
Richard Groves

또한 IQKeyboardManager.shared.enable = true키보드가 내 텍스트 필드를 숨기지 않도록 설정해야했습니다 . 어쨌든 이것이 최선의 해결책입니다. 수직으로 배열 된 4 개의 필드가 있고 다른 솔루션은 최하위 필드에서 작동하지만 맨 위는 시야에서 벗어납니다.
MisterEd

12

ScrollView키보드가 나타날 때 콘텐츠를 스크롤 할 수 있도록를 추가 하고 키보드 크기의 하단 패딩을 설정 해야합니다 .

키보드 크기 NotificationCenter를 확인하려면를 사용하여 키보드 이벤트에 등록 해야합니다 . 이를 위해 사용자 정의 클래스를 사용할 수 있습니다.

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

BindableObject적합성은 당신이로이 클래스를 사용 할 수 있도록 State하고 뷰 업데이트를 트리거합니다. 필요한 경우 튜토리얼을 참조하십시오 BindableObject: SwiftUI 튜토리얼

그것을 얻으면 ScrollView키보드가 나타날 때 크기를 줄 이도록 a 를 구성해야합니다 . 편의상 이것을 ScrollView어떤 종류의 구성 요소로 래핑했습니다 .

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

이제해야 할 일은 사용자 정의 안에 콘텐츠를 포함하는 것 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)
    }
}

편집 : 키보드가 숨어있을 때 스크롤 동작이 정말 이상합니다. 애니메이션을 사용하여 패딩을 업데이트하면이 문제가 해결되거나 padding스크롤보기 크기를 조정하기 위해를 사용하는 것이 좋습니다 .


bindableobject에 대한 경험이있는 것 같습니다. 원하는대로 작동 할 수 없습니다. : 당신은 살펴 수 있다면 좋은 일 것입니다 stackoverflow.com/questions/56500147/...
SwiftiSwift

왜 @ObjectBinding를 사용하지 않는
SwiftiSwift

3
BindableObject더 이상 사용 되지 않으므로 불행히도 더 이상 작동하지 않습니다.
LinusGeffarth

2
@LinusGeffarth 가치 BindableObject를 위해 ObservableObject, 및 didChange로 이름이 변경 되었습니다 objectWillChange. (내가 사용 테스트하지만 목적은 잘보기를 업데이트 @ObservedObject대신 @State)
SeizeTheDay

안녕하세요,이 솔루션은 콘텐츠를 스크롤하지만 텍스트 필드의 절반을 숨기는 키보드 위에 흰색 영역이 표시됩니다. 흰색 영역을 제거 할 수있는 방법을 알려주십시오.
Shahbaz Sajjad

6

.keyboardAware()수정자를 제공하는 편리한 SPM 패키지로 기존 솔루션을 검토하고 리팩토링했습니다 .

KeyboardAwareSwiftUI

예:

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

    }
}

출처:

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

textview 높이의 절반 만 보입니다. 이 문제를 해결하는 방법을 알고 있습니까?
Matrosov Alexander

좋은. 이것은 내 시간을 절약했습니다. 사용하기 전에이 수정 자 핸들 뷰의 하단 패딩을 알고 있습니다.
Brownsoo 한

5

Benjamin Kindle의 답변을 시작점으로 사용했지만 해결하고 싶은 몇 가지 문제가있었습니다.

  1. 여기에서 대부분의 답변은 키보드 프레임 변경을 다루지 않으므로 사용자가 키보드를 화면에 표시 한 상태에서 장치를 회전하면 깨집니다. keyboardWillChangeFrameNotification처리 된 알림 목록에 추가하면 이 문제가 해결됩니다.
  2. 비슷하지만 다른 맵 클로저를 사용하는 여러 게시자를 원하지 않았기 때문에 세 가지 키보드 알림을 모두 단일 게시자로 연결했습니다. 긴 체인이지만 각 단계는 매우 간단합니다.
  3. 다른 뷰처럼 뷰를 사용 하고 콘텐츠 뷰를 매개 변수로 전달하는 대신 후행 클로저로 콘텐츠를 전달할 수 init있도록를 허용하는 함수를 제공 했습니다 .@ViewBuilderKeyboardHostinit
  4. Tae과 fdelafuente가 의견에서 제안했듯이 Rectangle하단 패딩을 조정하기 위해를 교체했습니다 .
  5. 대신 하드 코딩 "UIKeyboardFrameEndUserInfoKey"문자열을 사용하는 전에서 제공하는 문자열을 사용하고 싶었 UIWindow등을 UIWindow.keyboardFrameEndUserInfoKey.

그것을 모두 모아서 :

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


이 솔루션은 증가 작동하지 않습니다 Keyboard높이
GSerjo

@GSerjo에서보고있는 문제에 대해 자세히 설명해 주시겠습니까? 이 코드를 내 앱에서 사용하고 있으며 잘 작동합니다.
Timothy Sanders

PridictiveiOS에서 켜주시겠습니까 keyboard? Settings-> General-> Keyboard-> Pridictive. 이 경우 계산을 수정하지 않고 키보드에 패딩을 추가합니다
GSerjo

@GSerjo : iOS 13.1 베타를 실행하는 iPad Touch (7 세대)에서 예측 텍스트를 활성화했습니다. 예측 행의 높이에 패딩을 올바르게 추가합니다. (중요한 점은 여기서는 키보드 의 높이를 조정하는 것이 아니라 뷰 자체 패딩 을 것입니다.) 주석 처리 된 디버깅 맵에서 스왑을 시도하고 예측에 대해 얻은 값으로 재생 해보십시오. 건반. 다른 댓글에 로그를 게시하겠습니다.
Timothy Sanders 19 년

"디버깅"맵의 주석 처리를 제거하면 할당 된 값을 볼 수 있습니다. keyboardHeight . 내 iPod Touch (세로)에서 예측 기능이있는 키보드는 254 포인트입니다. 그것 없이는 216 점입니다. 화면에 키보드를 사용하여 예측 기능을 끌 수도 있으며 패딩이 제대로 업데이트됩니다. 자동 완성 기능이있는 키보드 추가 : 자동 Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders

5

솔직히 말해서이 답변의 많은 부분이 정말 부풀어 보입니다. SwiftUI를 사용하는 경우 Combine을 사용할 수도 있습니다.

KeyboardResponder아래와 같이 생성하면 이전에 설명한대로 사용할 수 있습니다.

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

나는 키보드가 나타납니다있는 애니메이션과 일치하는 .animation (.easeIn를) 추가
미키 엘 펠트

(아이폰 OS 13이 답변의 역사로 이동)
로리 원수

1
안녕하세요. .assign (to : \ .keyboardHeight)에서 "컨텍스트에서 키 경로 유형을 추론 할 수 없습니다. 루트 유형을 명시 적으로 지정하는 것이 좋습니다."라는 오류 메시지가 표시됩니다. ios 13 및 ios 14 모두에 대한 적절하고 깨끗한 솔루션을 알려주십시오.
Shahbaz Sajjad

UIResponder.keyboardWillHideNotification에 대한 다른 리스너를 추가해야했습니다. 그 외에는 이것이 나를 위해 일한 유일한 솔루션입니다. 감사합니다!
Antonín Karásek

4

이것은 @kontiki가 만든 것에서 수정되었습니다. 스크롤해야하는 필드가 NavigationView 내부 양식의 일부인 베타 8 / GM 시드에서 앱에서 실행 중입니다. KeyboardGuardian은 다음과 같습니다.

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

그런 다음 열거 형을 사용하여 rects 배열의 슬롯과 총 수를 추적했습니다.

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

KeyboardSlots.count.rawValue필요한 어레이 용량입니다. 다른 것들은 rawValue로 .background (GeometryGetter) 호출에 사용할 적절한 인덱스를 제공합니다.

설정하면 다음과 같이 KeyboardGuardian에서 뷰를 볼 수 있습니다.

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

실제 움직임은 다음과 같습니다.

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

보기에 첨부. 제 경우에는 전체 NavigationView에 연결되어 있으므로 키보드가 나타나면 전체 어셈블리가 위로 올라갑니다.

SwiftUI를 사용하여 10 진수 키보드에서 완료 도구 모음 또는 리턴 키를 얻는 문제를 해결하지 못 했으므로 대신 이것을 사용하여 다른 곳에서 탭할 때 숨 깁니다.

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

다음과 같이 뷰에 첨부합니다.

.modifier(DismissingKeyboard())

일부 뷰 (예 : 선택기)는 첨부하는 것을 좋아하지 않으므로 수정자를 가장 바깥 쪽 뷰에 두드리는 것보다 좀 더 세분화해야 할 수도 있습니다.

열심히 일한 @kontiki에게 감사드립니다. 위의 GeometryGetter가 여전히 필요합니다 (아니요, 그가 그의 예에서 설명하는 것처럼 환경 설정을 사용하도록 변환하는 작업도 수행하지 않았습니다).


1
반대표를 던진 개인에게 : 왜? 내가보기에 내가 잘못 방법을 알고 싶습니다 그래서, 유용한 무언가를 추가하려고 시도
Feldur

4

위의 몇 가지 솔루션에는 몇 가지 문제가 있었으며 반드시 "가장 깨끗한"접근 방식은 아니 었습니다. 이 때문에 아래 구현을 위해 몇 가지 사항을 수정했습니다.

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

콘텐츠를 오프셋하는 방법에 대해 더 깔끔한 접근 방식과 책임을 생성 된 뷰 (수정자가 아님)로 옮기는 것이 좋지만 오프셋 코드를 뷰로 이동할 때 게시자가 제대로 트리거하도록 할 수없는 것 같습니다. ...

또한 final class현재 알 수없는 예외 크래시 (인터페이스 요구 사항을 충족하더라도)가 발생하므로이 인스턴스에서 게시자를 사용해야하며 오프셋 코드를 적용 할 때 전반적인 ScrollView가 가장 좋은 방법입니다.


아주 좋은 솔루션, 적극 권장합니다! 키보드가 현재 활성화되었는지 여부를 나타내는 부울을 추가했습니다.
Peanutsmasher

3

SwiftUI의 전환 / 애니메이션 API가 완료되었는지 확실하지 않지만 다음 CGAffineTransform과 함께 사용할 수 있습니다 ..transformEffect

다음과 같이 게시 된 속성을 사용하여 관찰 가능한 키보드 개체를 만듭니다.

    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
    }

}

그런 다음 해당 속성을 사용하여 다음과 같이보기를 재정렬 할 수 있습니다.

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

또는 더 간단

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

나는이 대답을 좋아하지만 'ScreenSize.portrait'가 어디에서 왔는지 잘 모르겠습니다.
Misha Stone

안녕하세요 @MishaStone 감사합니다. ScreenSize.portrait 내가 방향 및 비율에 스크린 기반의 측정을 얻기 위해 만들어진하는 클래스입니다 ....하지만 당신은 당신이 당신의 번역에 필요한 값으로 대체 할 수
blacktiago

3

Xcode 12 베타 4는 ignoresSafeArea이제 키보드를 피하는 데 사용할 수 있는 새로운보기 수정자를 추가합니다 .

.ignoresSafeArea([], edges: [])

이렇게하면 키보드와 모든 안전 영역 가장자리를 피할 수 있습니다. .keyboard피하지 않으려면 첫 번째 매개 변수를로 설정할 수 있습니다 . 적어도 내 뷰 계층 구조 설정에는 몇 가지 단점이 있지만 이것이 Apple이 키보드를 피하기를 원하는 방식 인 것 같습니다.


2

여기에서 복사 된 답변 : SwiftUI를 사용하여 항상 키보드 상단에 TextField

나는 다른 접근 방식을 시도했지만 그들 중 누구도 나를 위해 일하지 않았습니다. 아래는 다른 장치에서 작동하는 유일한 것입니다.

다음 확장자를 파일에 추가하십시오.

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

보기에서 offsetValue를 바인딩하려면 변수가 필요합니다.

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

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

2
그냥 참고로, 당신은 전화 개체를 소유하고 NotificationCenter.default.addObserver당신은 ... 사람들을 저장하고 적절한 시간에 관찰자를 제거해야 ...
TheCodingArt

안녕하세요 @TheCodingArt, 맞습니다. 이와 같이 시도해 보았지만 ( oleb.net/blog/2018/01/notificationcenter-removeobserver ) 나에게 작동하지 않는 것 같습니다.
Ray

2

Mark Krenek과 Heiko가 지적했듯이 Apple은 마침내 Xcode 12 beta 4에서이 문제를 해결하는 것처럼 보였습니다. 상황은 빠르게 진행되고 있습니다. 2020 년 8 월 18 일에 게시 된 Xcode 12 베타 5 의 릴리스 노트에 따르면 "양식, 목록 및 텍스트 편집기는 더 이상 키보드 뒤에 콘텐츠를 숨기지 않습니다. (66172025)". 방금 다운로드하고 며칠 전에 시작한 앱의 Form 컨테이너를 사용 하여 베타 5 시뮬레이터 (iPhone SE2) 에서 빠른 테스트를 수행 했습니다.

이제 TextField에 대해 "그냥 작동"합니다 . SwiftUI는 키보드를위한 공간을 만들기 위해 캡슐화 Form에 적절한 하단 패딩을 자동으로 제공합니다. 그리고 자동으로 Form을 위로 스크롤하여 키보드 바로 위에 TextField를 표시합니다. ScrollView 컨테이너는 이제 키보드가 나타날 때도 잘 작동합니다.

그러나 Андрей Первушин이 주석에서 지적했듯이 TextEditor에는 문제가 있습니다. 베타 5 및 6은 키보드를위한 공간을 만들기 위해 캡슐화 양식에 적절한 하단 패딩을 자동으로 제공합니다. 그러나 양식을 자동으로 위로 스크롤하지는 않습니다. 키보드가 TextEditor를 덮을 것입니다. 따라서 TextField와 달리 사용자는 TextEditor를 표시하려면 Form을 스크롤해야합니다. 버그 보고서를 제출하겠습니다. 아마도 베타 7이 그것을 고칠 것입니다. 너무 가까이 ...

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


베타 5 및 베타 6에서 테스트 된 Apple 릴리스 노트가 표시되고, TextField가 작동하지만 TextEditor가 아닙니다. 무엇을 놓칠 수 있습니까? @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)}}}
Андрей Первушин

2

용법:

import SwiftUI

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

암호:

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

솔루션을 시도했습니다 ...보기가 텍스트 필드의 절반으로 만 스크롤됩니다. 위의 모든 솔루션을 시도했습니다. 같은 문제가 있습니다. 도와주세요!!!
Sona

@Zeona, 간단한 앱으로 시도해보세요. 뭔가 다른 작업을하고있을 수 있습니다. 또한 안전 영역을 사용하는 경우 '-(UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0)'을 제거하십시오.
8suhas

제거 (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0) 키보드 위에 공백이 생겼습니다
Sona

1

처리 TabView'들

나는 Benjamin Kindle의 대답을 좋아 하지만 TabViews를 지원하지 않습니다. 다음은 TabView를 처리하기위한 그의 코드에 대한 조정입니다.

  1. UITabView프레임이 설정되었을 때 tabView의 크기를 저장할 확장자를 추가하십시오 . 일반적으로 프로젝트에 tabView가 하나만 있기 때문에 정적 변수에 저장할 수 있습니다 (여러 개가있는 경우 조정해야 함).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. 탭 바의 높이를 고려 onReceive하여 KeyboardHost보기 하단에서 변경해야합니다 .
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. 그리고 그게 다야! 매우 간단합니다 🎉.

1

확장 UIHostingController하고 조정 하여 완전히 다른 접근 방식을 취 했습니다 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()
    }
}

그런 다음 SceneDelegate사용하도록 변경하십시오.MyHostingController 대신UIHostingController .

완료되면 SwiftUI 코드 내부의 키보드에 대해 걱정할 필요가 없습니다.

(참고 :이 작업의 의미를 완전히 이해하기 위해 아직 충분히 사용하지 않았습니다!)


1

이것이 SwiftUI에서 키보드를 처리하는 방법입니다. 기억해야 할 점은 연결된 VStack에서 계산을한다는 것입니다.

뷰에서 수정 자로 사용합니다. 이 방법:

struct LogInView: View {

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

따라서이 수정자를 사용하려면 먼저 VStack에서 선택한 TextField 위치를 가져 오는 UIResponder의 확장을 만듭니다.

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

이제 Combine을 사용하여 KeyboardModifier를 만들어 키보드가 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() }
    }
  }
}

1

iOS 14 (베타 4)의 경우 매우 간단하게 작동합니다.

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

보기의 크기는 키보드 상단에 맞춰 조정됩니다. frame (.maxHeight : ...) 등을 사용하면 확실히 더 많은 개선이 가능합니다.

안타깝게도 iPad의 플로팅 키보드는 이동할 때 여전히 문제를 일으 킵니다. 그러나 위에서 언급 한 솔루션도 마찬가지이며 아직 베타 버전이므로 알아낼 수 있기를 바랍니다.

드디어 Thx Apple!


이것은 전혀 작동하지 않습니다 (14.1). 아이디어는 무엇입니까?
Burgler-dev 2010

0

내보기 :

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 :

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

기본적으로 키보드 높이를 기준으로 하단 패딩을 관리합니다.


-3

내가 이것에 대해 관리 한 가장 우아한 대답은 rraphael의 솔루션과 유사합니다. 키보드 이벤트를 수신하는 클래스를 만듭니다. 패딩을 수정하기 위해 키보드 크기를 사용하는 대신 키보드 크기의 음수 값을 반환하고 .offset (y :) 수정자를 사용하여 가장 바깥 쪽 뷰 컨테이너의 오프셋을 조정합니다. 애니메이션 효과가 충분하며 모든 뷰에서 작동합니다.


애니메이션을 어떻게 얻었습니까? 을 가지고 .offset(y: withAnimation { -keyboard.currentHeight })있지만 콘텐츠가 애니메이션 대신 점프합니다.
jjatie 19

내가이 코드를 사용했던 것은 몇 가지 베타 전에 있었지만, 이전 코멘트 당시에는 런타임 동안 vstack의 오프셋을 수정하는 것이 필요한 전부였으며, SwiftUI는 변경 사항을 애니메이션으로 보여줄 것입니다.
pcallycat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.