ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI - 키보드 노출 여부에 따른 뷰 오프셋 조정
    SwiftUI 2024. 11. 29. 00:40

    안녕하세요. 그린입니다 🍏
    이번 포스팅에서는 키보드 노출 여부에 따라 뷰를 키보드에 가리지 않고 나타나도록 오프셋 조정하는 방법에 대해 한번 구현해볼까해요 🙋🏻
     


    키보드 노출 여부에 따른 뷰 오프셋 조정

    SwiftUI로 텍스트 필드나 텍스트 뷰를 사용할때 키보드가 노출되고 뷰의 영역에서 사용성 어려움을 겪는 경우가 종종있어요 😅

     
    예를들어 최하단에 텍스트 필드가 그려지고 있고 텍스트 필드를 포커싱 했을때, 키보드가 노출되는데, 키보드 영역이 어떠한 뷰들을 가리곤 합니다.
     

    이를 개선하기 위해서는 키보드가 노출될때 해당 텍스트 필드 영역의 오프셋을 조정하여 키보드에 가리지 않고 뷰를 다 보이도록 해볼 수 있어요!

     
    사실 SwiftUI는 기본적으로 키보드 회피 동작을 제공해주고 있어요.
    그래서 단순하게 텍스트 필드를 사용한다면 별도 코드 구현 없이도 아래와 같이 키보드가 나타나면 해당 뷰의 오프셋을 조정하여 가리지 않고 포커싱되게 해줍니다.
     

    import SwiftUI
    
    struct ContentView: View {
      @State var text: String = ""
      
      var body: some View {
        ScrollView {
          VStack {
            Text("상단 영역")
            
            Rectangle()
              .fill(.green)
              .frame(height: 500)
            
            TextField(
              "내용을 입력하세요.",
              text: $text
            )
            
            Text("하단 영역")
          }
        }
        .padding()
      }
    }

     

    이런 코드를 실제 돌려보면요.

     

     
    이렇게 해당 텍스트 필드에 포커싱이 가서 키보드가 노출되면 뷰의 오프셋을 조정하여 텍스트 필드에 어떤 글자를 쓰고 있는지 보여지게 해주죠.
     

    그런데 여기 의도치 않은 동작이 있어요.

     
    만약 우리가 "하단 영역" 이라는 텍스트까지 같이 노출시키고 싶을때는 기본 동작만으로 자동으로 제어가 되지 않습니다.
    해당 키보드를 띄우는 뷰가 주체가 되기 때문이죠.
     

    그렇기에 기본적으로는 SwiftUI에서 제공은 해주나, 완벽하게 우리가 원하는 동작으로 컨트롤 하기 위해서는 커스텀하게 만들어봐야 된다는 필요성을 느낍니다 🤔

     

    그래서 이런 시도를 한번 해보려고 합니다!

     
    관련하여 많은 토의가 오갔던 스택 오버플로를 참고하여 1차적으로 만들어보고 또 개선하여 다른 방식으로 만든걸 소개해볼까요 🙋🏻
     

    Move TextField up when the keyboard has appeared in SwiftUI

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

    stackoverflow.com

     


    1차 방식 (스택 오버플로)

    위 레퍼런스에서 정말 다양한 방법이 있었지만, 저는 rraphael이라는 유저의 솔루션이 가장 심플하고 적용하기 좋았던것 같아서 소개해봅니다.
     

    바로 이 방식입니다.

     

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

     

    단순하게 UIResponder를 통해서 키보드가 나타나거나 가려질때 키보드 사이즈의 높이를 파악해서 해당 객체를 가진 뷰에 알려주는 방식이에요!

     
    즉, 노티피케이션의 간단한 방식입니다.
     

    해당 코드는 아래와 같이 적용될 수 있습니다.

     

    import SwiftUI
    
    struct ContentView: View {
      @StateObject private var keyboard = KeyboardResponder()
      @State var text: String = ""
      
      var body: some View {
        VStack {
          Text("상단 영역")
          
          Spacer()
          
          Rectangle()
            .fill(.green)
            .frame(height: 700)
          
          TextField(
            "내용을 입력하세요.",
            text: $text
          )
          
          Text("하단 영역")
        }
        .padding(.bottom, keyboard.currentHeight)
      }
    }

     
    StateObject로 해당 옵저버블 객체를 선언해줘야 합니다.
     
    사실 ObservedObject로 해도 iOS 16 이상에서는 큰 문제가 없지만, iOS 15 이하에서는 키보드가 떨리면서 제대로 안올라오고 그런 버그 현상이 나타날거에요.
    이는 iOS 15에서 SwiftUI View 업데이트 로직이 iOS 16 이상과 조금은 다르게 동작하기 때문이죠.
     
    ObservedObject일 때, 뷰가 재생성 될 때마다 초기화가 되어서 키보드 상태를 일관되게 유지할 수 없는거죠.
    즉, KeyboardResponder가 새로 초기화되서 키보드 상태가 의도치 않게 변하게 됩니다.
    그렇기에 꼭 의존성 주입이 아니라 인스턴스가 초기화 생성되는 시점에서는 애플의 권장에 따라 StateObject를 사용해야 합니다.
     

    그럼 동작이 어떻게 되는지 볼까요?

     

     
    이렇게 의도대로 텍스트 필드에 포커싱이 가면 하단 영역까지 보이도록 키보드 노출과 함께 올라오죠.
     
    해당 VStack의 하단 패딩으로 키보드 높이를 감지하여 제약을 주었기에 가능해지는 결과입니다.
     

    그럼 이걸 조금 더 사용하기 편하게 만들어볼까요?

     


    2차 방식 (Combine, View 확장)

    Combine과 뷰에서 쉽게 사용할 수 있도록 모디파이어화 시켜볼 수 있습니다.

     

    import Combine
    import SwiftUI
    
    struct KeyboardAdaptive: ViewModifier {
      @State private var keyboardHeight: CGFloat = 0
      
      private let keyboardWillShow = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .compactMap { notification in
          notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
        }
        .map { rect in
          rect.height
        }
      
      private let keyboardWillHide = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in CGFloat(0) }
      
      func body(content: Content) -> some View {
        content
          .padding(.bottom, keyboardHeight)
          .onReceive(
            Publishers.Merge(keyboardWillShow, keyboardWillHide)
          ) { height in
            withAnimation(.easeInOut) {
              self.keyboardHeight = height
            }
          }
      }
    }
    
    extension View {
      func keyboardAdaptive() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAdaptive())
      }
    }

     
    요렇게 기존 방식처럼 키보드를 높이를 계산하는건 마찬가지입니다.
    Combine을 통해 비동기 처리를 하고 모디파이어로 만들어 더 쓰게 쉽죠.
     

    적용은 간단히 이렇게 하면 됩니다!

     

    struct ContentView: View {
      @State var text: String = ""
      
      var body: some View {
        VStack {
          Text("상단 영역")
          
          Spacer()
          
          Rectangle()
            .fill(.green)
            .frame(height: 700)
          
          TextField(
            "내용을 입력하세요.",
            text: $text
          )
          
          Text("하단 영역")
        }
        .keyboardAdaptive()
      }
    }

     

    간단하죠?

     
    동작은 위와 같아서 생략합니다!
     
    요렇게 하면 키보드 노출 여부에 따라 뷰의 오프셋을 조정하여 가리지 않고 보여줄 수 있습니다 😃
     


    레퍼런스

    Move TextField up when the keyboard has appeared in SwiftUI

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

    stackoverflow.com

Designed by Tistory.