프로젝트를 진행하면서 편지 형식으로 텍스트를 입력하는 기능을 개발하게 되어, 편지지를 구현하고 그 과정을 블로그에 남기려고 합니다.
보통 편지의 구조를 생각할 때, 글을 적는 부분을 나타내기 위해 일정한 간격으로 밑줄을 그어 놓습니다.
하지만 SwiftUI에서 TextEditor로 멀티라인 텍스트를 입력할 수 있지만, 줄 사이에 구분선을 넣는 기능은 제공되지 않습니다.
이를 구현하기 위한 방법을 생각해봤습니다.
- 편지의 한 줄마다 텍스트를 입력할 수 있는 TextField를 사용하고, 각 TextField 바로 아래에 밑줄을 두어 주어진 크기를 넘어가거나 개행문자 \n이 입력되면 다음 TextField로 포커스를 넘기는 방식
- 여러 개의 밑줄을 그리고 TextEditor를 겹쳐서 줄 위에 글을 작성하는 것처럼 보이게 하는 방식
1번 방법은 입력되는 텍스트를 관리하는 것이 2번 방법보다 번거롭고, TextEditor에는 이미 주어진 width 크기를 넘어가면 자동으로 다음 줄로 넘어가는 기능이 있기 때문에 2번 방법을 선택하여 개발했습니다.
1. 편지지 밑줄 및 편지 입력 View 겹치기
1.1 textEditor 사용
우선 편지지의 배경과 밑줄을 만들고 textEditor를 겹쳤습니다.
여기서 문제가 생겼는데, TextEditor가 투명하지 않아 편지지를 가리게 되었습니다.
배경색을 없애기 위해 .scrollContentBackground 모디파이어를 사용했지만, iOS 16.0 이상부터 사용 가능하여 이를 적용할 수 없었습니다.
1.2 UIViewRepresentable 사용
TextEditor를 투명하게 만들기 위해 SwiftUI만으로는 해결이 어려워 UIKit을 직접 수정했습니다.
UIKit을 기반으로 텍스트 입력 컴포넌트를 생성하려면, SwiftUI에서는 UIViewRepresentable을 사용해야 했습니다.
UIViewRepresentable
-> SwiftUI에서 UIKit의 UIView를 사용할 수 있도록 해주는 프로토콜
SwiftUI의 textEditor는 UIKit의 UITextView를 기반으로 동작하기 때문에 리턴 타입을 UITextView로 설정합니다.
2. 줄 간격 설정
줄 간격을 설정하지 않아 글자를 많이 작성하다보면 밑줄가 겹치게 되는 문제가 생겼습니다.
NSMutableParagraphStyle()를 사용해서 줄 간격과 텍스트 시작 위치를 설정했습니다.
줄 간격을 설정하여 이제는 밑줄과 겹치지 않게 되었습니다.
3. 편지 높이에 따른 줄 개수
3.1 이론
다음으로는 편지 높이에 맞게 줄 개수가 생성되고, 그에 알맞게 텍스트가 입력되도록 하겠습니다.
먼저, 크기에 따른 줄의 개수를 구해야 합니다.
줄의 개수는 폰트의 높이, 줄 간격, 최대 높이를 고려하면 됩니다.
maxLines (줄의 최대 개수)
편지의 최대 높이 ÷ (폰트의 높이 + 줄 간격)
여기서 maxLines의 값이 정수가 아니면 소수점을 다 버리면 됩니다(ex: 9.37 => 9)
시스템 폰트 18의 높이는 21.5이고, 줄 간격은 9.5이기 때문에, 최대 높이에서 31을 나누면 줄 수를 알 수 있습니다.
이제 SwiftUI에서 줄 간격을 31로 설정하고 적용해보겠습니다.
3.2 이론 적용
위에서 계산한 이론이 잘 적용되는지 경계값 위주로 대입해 분석해보겠습니다.
3.2.1 줄이 5개 일때
먼저 줄이 5개일 때, 즉 155(31 X 5)를 대입해보겠습니다.
3.2.2 줄이 17개 일때
줄이 17개일 때, 즉 527(31 x 17)을 대입해보겠습니다.
크기에 상관없이 7의 오차가 생기는 것을 확인했고 텍스트를 입력하는 곳에 크기를 7 더해주어 문제를 해결했습니다.
3.3 크기마다 텍스트 입력 시작점에 오차 발생
이번에는 크기에 따라 밑줄과 텍스트가 입력되는 부분에 오차가 생기는 것을 확인했습니다.
줄이 6개일 때 예시를 들어보겠습니다.
높이가 186~216일 때 줄이 6개가 생성되는데, 최소 높이와 최대 높이에서 차이가 있는 것을 확인할 수 있었습니다
현재는 밑줄을 그려주는 부분에 공백 높이를 50으로 설정했었는데, 이 값을 높이가 증가함에 따라 다르게 설정해야 하기 때문에, 높이에서 31의 나머지를 빼주는 방식으로 오차를 없앴습니다.
오차없이 텍스트의 시작 높이가 같아졌습니다.
4. 편지지 크기를 초과할 때, 스크롤 뷰로 변경되는 문제
입력되는 텍스트가 편지지를 초과하면 스크롤 뷰로 변하는 문제가 있습니다.
4.1 클래스 정의
편지지의 크기, 입력된 텍스트, 커서의 위치 등 여러 정보를 처리해야 하며, 텍스트가 편지지를 넘어갈 경우 입력을 제한해야 하므로 UITextViewDelegate 프로토콜을 채택한 클래스를 만들어야 합니다.
그 후, UIViewRepresentable의 makeCoordinator 함수를 사용하여 위 클래스의 델리게이트를 할당합니다.
할당을 하면 UITextView의 동작을 제어할 수 있습니다.
델리게이트
-> 델리게이트는 이벤트나 작업을 처리할 객체를 지정하는 역할
4.2 텍스트 입력 막기
크기를 초과할 경우 텍스트를 입력하지 못하게 하려는 목표가 있습니다. 우선 텍스트 입력을 막는 방법을 찾아보겠습니다
UITextViewDelegate에서 텍스트를 제어하는 함수 2개를 알아보겠습니다
textViewDidChange
-> 사용자가 텍스트를 변경한 후에 호출되는 메서드
shouldChangeTextIn -> (Bool)
-> 사용자가 텍스트를 변경하기 전에 호출
-> 텍스트가 수정될지 여부를 결정 가능(Bool)
사용자가 텍스트를 변경하기 전에 호출되고 텍스트가 변경 될 지를 결정할 수 있는 shouldChangeTextIn을 사용하는게 맞다고 판단했습니다.
4.3 편지지 크기 초과 로직
4.3.1 줄 수 구하기
이제 크기를 초과하는지 확인하는 로직을 만들어야 합니다.
NSLayoutManager를 사용하면 글리프의 개수나 한 줄에 있는 글리프의 범위 등을 알 수 있어서 줄 수를 계산하는 데 오차 없이 계산할 수 있습니다.
글리프
-> 화면에 보이는 글자 / 폰트 등 사용자가 화면에 보이는 글자에 대한 정보
shouldChangeTextIn은 사용자가 텍스트를 입력하고 변경 사항이 적용되기 전에 호출되기 때문에 그 메서드 안에서 텍스트는 변경되지 않습니다. 그래서 텍스트를 수동으로 추가해줘야합니다.
기존에 입력되어있는 텍스트 (ex: "안녕하ㅅ")와 새로 추가할 텍스트 (ex: "ㅔ") 그리고 변경할 위치를 입력하여 만들어진 newText("안녕하세")를 미리 만들어 줍니다.
텍스트 레이아웃을 관리하는 객체인 NSLayoutManager, 텍스트의 영역을 나타내는 객체인 NSTextContainer, 텍스트의 폰트 속성 등 정보를 담고 있는 NSTextStorage 객체를 인스턴스화하고 서로 연결해줍니다.
NSLayouyManger에 함수를 만들어줍니다.
이 함수는 총 글리프 수를 기반으로 반복하여 각 글리프가 포함된 줄의 범위를 가져옵니다. 각 줄의 범위가 결정되면,
numberOfLines를 1씩 증가시켜 총 줄 수를 계산합니다. 반복문 내에서, 현재 글리프가 속한 줄의 범위를lineFragmentRect(forGlyphAt:effectiveRange:) 메서드를 통해 구하고, 그 후 NSMaxRange(lineRange)를 사용하여 인덱스를 해당 줄의 마지막 글리프 위치로 설정합니다. 이렇게 하면, 각 줄을 하나씩 계산하며 다음 줄부터 계속 진행할 수 있습니다. 결국, 최종적으로 전체 줄 수를 반환합니다.
layoutManager의 numberOfLines() 메서드를 호출하여 텍스트의 총 줄 수를 totalLines에 저장합니다.
4.3.2 입력 막기
이제 줄 수를 가져왔으니, 편지를 넘어가는 경우 입력을 막아야 합니다.
편지를 넘어가는 경우는 3가지가 있습니다:
- 사용자가 엔터를 누름에 따라 편지지 크기를 넘어갈 때
- 사용자가 입력한 글자가 편지지의 width 크기를 넘어 자동으로 줄이 넘어갈 때
- 사용자가 가장 마지막 줄 마지막 글자에서 엔터를 막음( 이 경우 글리프에서 줄이 넘어갔다를 인지하지 못해 따로 막아야합니다)
위 3가지 경우는 아래와 같이 막으면 됩니다.
더이상 편지지를 넘어가지 않는 것을 확인할 수 있습니다.
깃허브에 해당 코드를 올려 놓았습니다
GitHub - Leeyounhyoung/LetterApp: Letter app that works in SwiftUI
Letter app that works in SwiftUI. Contribute to Leeyounhyoung/LetterApp development by creating an account on GitHub.
github.com
긴 글 읽어주셔서 감사합니다.
'ios 개발일지' 카테고리의 다른 글
[SwiftUI] 말풍선 구현하기: View 만들기, CustomTriangle, SpeechBubble (0) | 2025.03.11 |
---|---|
[SwiftUI] CoreData를 사용하여 채팅창 구현하기: Update와 Delete (0) | 2024.07.20 |
[Swiftui] Core Data를 사용하여 채팅창 구현하기: Create와 Read (0) | 2024.07.08 |
[Xcode] 탭 간격, 들여쓰기 간격 변경 및 Re-Indent (0) | 2024.07.06 |
[Xcode] Simulator 이전 버전 추가 (0) | 2024.04.12 |