Project/FirebaseTest

FireBase - UITextView with open WKWebView

밤새는 탐험가89 2024. 12. 3. 02:07
import UIKit

class RegisterController: UIViewController {

    ...
    private let termsTextView: UITextView = {
        let textView = UITextView()
        
        let attributedString = NSMutableAttributedString(string: "By creating an account, you agree to our Terms & Conditions and you acknowledge that you have read our Privacy Policy.")
        
        attributedString.addAttribute(.link, value: "terms://termsAndConditions", range: (attributedString.string as NSString).range(of: "Terms & Conditions"))
        
        attributedString.addAttribute(.link, value: "privacy://privacyPolicy", range: (attributedString.string as NSString).range(of: "Privacy Policy"))
        
        textView.linkTextAttributes = [.foregroundColor: UIColor.systemBlue]
        textView.backgroundColor = .clear
        textView.attributedText = attributedString
        textView.textColor = .label
        textView.isSelectable = true
        textView.isEditable = false
        textView.delaysContentTouches = false
        textView.isScrollEnabled = false
        return textView
    }()

    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.termsTextView.delegate = self
        
        ...
}


// MARK: Extensions
extension RegisterController: UITextViewDelegate {
    
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
        
        
        if URL.scheme == "terms" {
            self.showWebViewerController(with: "https://policies.google.com/terms?h1=en")
        } else if URL.scheme == "privacy" {
            self.showWebViewerController(with: "https://policies.google.com/privacy?h1=en")
        }
        
        return true
    }
    
    private func showWebViewerController(with urlString: String) {
        let vc = WebViewerController(with: urlString)
        let nav = UINavigationController(rootViewController: vc)
        self.present(nav, animated: true, completion: nil)
    }
    
    func textViewDidChangeSelection(_ textView: UITextView) {
        textView.delegate = nil
        textView.selectedTextRange = nil
        textView.delegate = self
    }
}

 

termsTextView와 관련된 코드 설명

1. termsTextView의 역할

  • termsTextView는 사용자가 회원가입을 진행하면서 약관 동의 및 개인정보 처리방침을 확인할 수 있도록 안내하는 텍스트 뷰입니다.
  • UITextView를 사용하여 링크를 포함한 약관 내용을 표시하며, 사용자가 특정 텍스트(예: "Terms & Conditions", "Privacy Policy")를 클릭하면 해당 링크로 이동합니다.

 

2. termsTextView 초기화

private let termsTextView: UITextView = {
    let textView = UITextView()
    
    let attributedString = NSMutableAttributedString(string: "By creating an account, you agree to our Terms & Conditions and you acknowledge that you have read our Privacy Policy.")
    
    attributedString.addAttribute(.link, value: "terms://termsAndConditions", range: (attributedString.string as NSString).range(of: "Terms & Conditions"))
    
    attributedString.addAttribute(.link, value: "privacy://privacyPolicy", range: (attributedString.string as NSString).range(of: "Privacy Policy"))
    
    textView.linkTextAttributes = [.foregroundColor: UIColor.systemBlue]
    textView.backgroundColor = .clear
    textView.attributedText = attributedString
    textView.textColor = .label
    textView.isSelectable = true
    textView.isEditable = false
    textView.delaysContentTouches = false
    textView.isScrollEnabled = false
    return textView
}()

 

주요 설정

  1. 텍스트와 스타일 설정:
    • 기본 텍스트: 약관 동의를 설명하는 문자열을 생성.
    • 하이퍼링크 설정:
      • NSMutableAttributedString을 사용해 "Terms & Conditions"와 "Privacy Policy"에 링크를 추가.
      • 링크는 커스텀 URL 스키마(예: terms:// 또는 privacy://)로 정의.
    • 텍스트 스타일:
      • 링크 색상: 파란색 (systemBlue).
      • 배경색: 투명 (clear).
      • 텍스트 색상: 시스템 라벨 색상(label)로 설정해 다크/라이트 모드 호환.
  2. 사용자 상호작용 설정:
    • isSelectable = true: 사용자가 텍스트를 클릭할 수 있도록 설정.
    • isEditable = false: 텍스트 편집 불가.
    • isScrollEnabled = false: 텍스트가 자동으로 스크롤되지 않도록 설정.

 

UITextViewDelegate와 extension

UITextViewDelegate를 통한 하이퍼링크 처리

 

termsTextView가 하이퍼링크를 지원하기 위해 UITextViewDelegate를 구현합니다.

 

1.shouldInteractWith URL:

  • 사용자가 termsTextView의 링크를 클릭했을 때 호출됩니다.
  • URL 스키마(scheme)에 따라 적절한 동작을 수행.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    if URL.scheme == "terms" {
        self.showWebViewerController(with: "https://policies.google.com/terms?h1=en")
    } else if URL.scheme == "privacy" {
        self.showWebViewerController(with: "https://policies.google.com/privacy?h1=en")
    }
    return true
}

 

작동 방식

  • URL 스키마 검사:
    • "terms": "Terms & Conditions" 링크 클릭 시 Google 약관 페이지로 이동.
    • "privacy": "Privacy Policy" 링크 클릭 시 Google 개인정보 처리방침 페이지로 이동.
  • showWebViewerController 호출:
    • WebViewerController를 생성해 URL 콘텐츠를 표시.

 

2. showWebViewerController(with urlString: String)

private func showWebViewerController(with urlString: String) {
    let vc = WebViewerController(with: urlString)
    let nav = UINavigationController(rootViewController: vc)
    self.present(nav, animated: true, completion: nil)
}

 

역할

  • 전달받은 urlString을 이용해 WebViewerController를 생성.
  • UINavigationController를 감싸 모달로 표시.

 

3.textViewDidChangeSelection:

func textViewDidChangeSelection(_ textView: UITextView) {
    textView.delegate = nil
    textView.selectedTextRange = nil
    textView.delegate = self
}

 

역할

  • 텍스트 뷰에서 텍스트 선택 동작을 무효화.
  • 사용자가 텍스트를 드래그하거나 선택하려고 시도하면 이를 방지하고 초기 상태로 복원.

 

코드 동작 흐름

  1. termsTextView에 약관 설명과 링크가 포함된 텍스트를 설정.
  2. 사용자가 "Terms & Conditions" 또는 "Privacy Policy"를 클릭:
    • UITextViewDelegate의 shouldInteractWith URL 호출.
    • 링크의 scheme에 따라 적절한 URL로 WebViewerController를 표시.
  3. 링크 클릭 외 텍스트 선택 동작이 발생하면 textViewDidChangeSelection으로 이를 무효화

 

🔥 URL 스키마(URL Scheme)를 사용하는 것과 직접 URL 주소를 입력하는 것의 차이를 이해하기 위해, 두 방식의 개념과 장단점을 비교해 보겠습니다.

 

1. URL 스키마란?

  • URL 스키마는 앱에서 특정 동작을 트리거하기 위한 사용자 정의 프로토콜입니다.
  • 일반적인 URL(https://example.com) 대신, 커스텀 스키마를 정의합니다(예: terms://termsAndConditions).
  • 이 방식은 URL을 입력했을 때 특정한 처리를 수행하도록 맞춤 설정할 수 있습니다.

 

2. URL 스키마와 직접 URL의 차이점

URL 스키마 (terms://) 직접 URL (https://)
사용 목적 앱 내부 로직을 처리하거나 특정 동작을 트리거 외부 리소스(웹 페이지, API 등)로 연결
유연성 동작을 앱에서 커스터마이즈 가능 URL에 접근하면 항상 동일한 리소스 반환
URL 처리 방식 커스텀 처리가 필요 (UITextViewDelegate 등 사용) URL을 웹 브라우저나 WebView로 직접 로드 가능
외부 서비스 연동 외부 웹 리소스와는 직접 연관되지 않음 외부 서비스에서 URL을 처리 가능
사용 사례 앱 내 특정 화면 이동, 비즈니스 로직 트리거 웹 페이지 로드, 외부 리소스 연결

 

3. URL 스키마의 필요성

1) 앱 내부 동작과 로직 처리

  • URL 스키마는 특정 URL에 반응하여 앱의 특정 로직을 실행할 수 있습니다.
  • 예를 들어:
    • terms://termsAndConditions → 약관 보기 화면 표시.
    • privacy://privacyPolicy → 개인정보 처리방침 화면 표시.
  • 이렇게 하면 URL 입력으로 앱 내의 다양한 동작을 효율적으로 구현할 수 있습니다.

2) 외부 URL에 종속되지 않음

  • URL 스키마를 사용하면 앱이 특정 동작을 실행하기 위해 외부 URL의 변경 여부에 영향을 받지 않습니다.
    • 예: 약관 URL이 변경되더라도 앱 내부의 스키마를 유지하면 로직을 변경할 필요가 없습니다.

3) 재사용성과 유지보수성 증가

  • URL 스키마를 통해 공통된 동작을 중앙에서 관리할 수 있습니다.
  • 앱 내부에서 처리해야 하는 동작은 스키마만 확인하면 쉽게 처리 가능하며, 유지보수가 용이합니다.

4) 복잡한 로직과의 통합

  • URL 스키마는 단순히 웹 페이지를 로드하는 것 이상으로 앱 내부 데이터 처리 또는 비즈니스 로직과 결합할 수 있습니다.
    • 예: app://product/123 → 특정 상품 화면으로 이동.
    • 이와 같은 방식으로 특정 데이터를 동적으로 전달받아 처리할 수도 있습니다.

 

4. 직접 URL 입력의 한계

  • 직접 URL을 입력하면 항상 해당 URL로 연결되기 때문에 고정된 동작만 수행합니다.
  • 예를 들어, https://example.com/terms를 입력하면 약관 페이지가 로드되지만, 앱 내에서 다른 처리를 할 수 있는 유연성이 부족합니다.

 

5. URL 스키마를 사용하는 실제 사례

  1. 앱 내 네비게이션
    • 특정 URL 스키마를 사용하여 앱 내부의 다른 화면으로 이동.
    • 예: myapp://settings → 앱 설정 화면 표시.
  2. 특정 이벤트 트리거
    • URL 스키마로 특정 이벤트를 트리거할 수 있음.
    • 예: myapp://loginSuccess → 로그인이 성공한 후 동작 실행.
  3. 웹과 앱 간의 통합
    • 웹 페이지에서 앱으로 특정 동작을 트리거할 때 사용.
    • 예: 앱 다운로드 링크에서 myapp://open을 사용해 앱 실행.
  4. 다양한 데이터 처리
    • URL 스키마에 데이터를 포함시켜 앱에서 이를 처리 가능.
    • 예: myapp://product?id=123 → 특정 상품 상세 화면 표시.

 

결론

URL 스키마를 사용하는 이유:

  1. 앱 내부에서 동작을 커스터마이즈할 수 있습니다.
  2. 외부 URL에 의존하지 않고 앱의 동작을 제어합니다.
  3. 특정 URL에 따른 복잡한 로직 처리 및 데이터 전달이 가능합니다.

직접 URL을 사용하는 경우:

  • 단순히 웹 페이지를 열거나 외부 리소스에 연결할 때 사용됩니다.

따라서, URL 스키마는 앱 내부 동작을 효율적으로 처리하거나, 동작을 커스터마이즈하고 외부 리소스와 독립적인 로직을 유지하고자 할 때 매우 유용합니다.

 

 

🔥 그럼 "terms://termsAndConditions", "privacy://privacyPolicy"을 사용한 이유?

위 코드에서 URL 스키마를 설정한 이유는 링크 클릭 시 앱 내부에서 커스텀 로직을 처리하기 위해서입니다. 단순히 하이퍼링크를 바로 걸지 않은 이유는 특정 조건에 따라 링크를 처리하거나, 사용자가 URL을 클릭했을 때 앱이 원하는 방식으로 동작하도록 만들기 위함입니다.

 

 

URL 스키마를 설정한 이유

1. 앱 내에서 동작을 제어하기 위해

  • terms://termsAndConditions와 privacy://privacyPolicy를 사용하면 **하나의 처리 로직(Delegate 메서드)**으로 클릭 이벤트를 감지하여, 조건에 따라 원하는 작업을 실행할 수 있습니다.
  • 예를 들어, 사용자가 클릭한 링크가 "Terms & Conditions"인지, "Privacy Policy"인지 확인하고 각 링크에 따라 다른 동작을 수행할 수 있습니다.

코드에서 이를 처리하는 부분:

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    if URL.scheme == "terms" {
        self.showWebViewerController(with: "https://policies.google.com/terms?h1=en")
    } else if URL.scheme == "privacy" {
        self.showWebViewerController(with: "https://policies.google.com/privacy?h1=en")
    }
    return true
}

 

여기서의 장점:

  • 클릭한 URL의 **스키마(terms 또는 privacy)**를 검사하고, 필요한 경우 다른 URL로 리디렉션하거나 추가 작업을 수행할 수 있음.
  • 예를 들어:
    • terms://를 클릭하면 약관 페이지를 앱 내의 뷰어로 표시.
    • 특정 링크 클릭 시 사용자 데이터를 로깅하거나 통계를 수집.

 

 

2. 유연성 증가

  • 직접 URL을 넣을 경우, "Terms & Conditions"와 "Privacy Policy"를 각각 특정 URL로 연결하게 됩니다. 이후 약관이나 정책 URL이 변경되면 모든 URL을 수정해야 합니다.
  • 반면 URL 스키마를 사용하면:
    • 스키마 자체는 고정 (terms://termsAndConditions).
    • URL 매핑을 코드 내부에서 관리할 수 있어 유지보수가 더 쉬워집니다.

예를 들어:

if URL.scheme == "terms" {
    self.showWebViewerController(with: "https://new-terms-url.com")
}

 

왜 하이퍼링크를 바로 걸지 않았을까?

하이퍼링크를 바로 걸었다면:

let attributedString = NSMutableAttributedString(string: "By creating an account, you agree to our Terms & Conditions and you acknowledge that you have read our Privacy Policy.")

attributedString.addAttribute(.link, value: "https://policies.google.com/terms?h1=en", range: (attributedString.string as NSString).range(of: "Terms & Conditions"))

attributedString.addAttribute(.link, value: "https://policies.google.com/privacy?h1=en", range: (attributedString.string as NSString).range(of: "Privacy Policy"))

 

이렇게 하면 링크를 클릭했을 때 해당 웹 페이지가 열리는 데 그치며, 추가 로직이나 동작을 제어할 방법이 없습니다.

 

결론

URL 스키마를 설정한 이유는 단순히 링크를 여는 것 이상으로, 앱 내에서 클릭 이벤트를 세분화하고, 유지보수성과 유연성을 높이기 위함입니다.

특히, 앱 내부에서 로직을 제어하고 싶거나 약관 및 정책과 같은 변경 가능성이 있는 리소스를 처리할 때 URL 스키마는 매우 효과적인 방법입니다.

 

 

🔥 value: "terms://termsAndConditions"와 URL.scheme == "terms" 사이의 관계는 URL의 구조에 따라 동작합니다. 이를 이해하려면 URL의 구성 요소와 URL.scheme의 역할을 살펴보아야 합니다.🔥

 

1. URL의 구조

URL은 아래와 같은 구조로 이루어져 있습니다:

<scheme>://<host>/<path>?<query>#<fragment>

 

부분의 의미:

  • scheme: URL의 프로토콜 또는 스키마를 나타냅니다. (예: https, http, terms 등)
  • host: 도메인 또는 리소스 위치. (예: www.example.com)
  • path: 호스트 내부에서 특정 리소스의 경로. (예: /termsAndConditions)
  • query: 추가적인 매개변수를 전달하기 위한 쿼리 문자열. (예: ?id=123)
  • fragment: 페이지 내 특정 위치를 나타내는 부분. (예: #section1)

 

2. 코드에서 사용된 URL

value: "terms://termsAndConditions"

 

이 URL은 다음과 같이 분해할 수 있습니다:

  • scheme: "terms"
  • host: 없음 (명시되지 않음)
  • path: "/termsAndConditions"

 

3. URL.scheme의 역할

URL.scheme

  • URL 객체의 scheme 프로퍼티는 URL의 스키마 부분만 반환합니다.
let url = URL(string: "terms://termsAndConditions")
print(url?.scheme) // 출력: "terms"

 

 

4. 코드 동작 설명

URL의 value 설정:

attributedString.addAttribute(.link, value: "terms://termsAndConditions", range: ...)

 

  • value로 설정된 URL은 terms://termsAndConditions입니다.
  • 사용자가 링크를 클릭하면 UITextViewDelegate의 shouldInteractWith 메서드로 전달되는 URL 객체에 이 값이 포함됩니다.

 

URL의 스키마 확인:

if URL.scheme == "terms" {
    self.showWebViewerController(with: "https://policies.google.com/terms?h1=en")
}

 

 

 

  • URL.scheme는 위에서 설정된 "terms://termsAndConditions" URL의 스키마(terms)를 반환합니다.
  • 따라서, "terms"와 일치하는지 확인하여 적절한 동작을 수행합니다.

 

6. 요약

  1. value: "terms://termsAndConditions"의 의미:
    • URL 전체를 설정하며, scheme은 "terms"이고 path는 "/termsAndConditions"입니다.
  2. URL.scheme:
    • URL에서 스키마 부분(즉, terms)만 추출합니다.
  3. 왜 스키마를 사용했는가:
    • 스키마를 통해 URL을 분류하고, 적절한 동작을 유연하게 처리할 수 있습니