viewHierarchy (2)

Kyu hyunSung·2025년 4월 20일

IOS

목록 보기
4/9
post-thumbnail

우리는 이제 이론을 대충 알았으니 실전으로 넘어갈거임

이 뷰를 한번 만들어볼거임

UI Design

Object Library에서 View Controller 생성

UILabel 3, UITextfield 1 적절히 배치

  • UITextField fontSize: 70/Bold
  • Aligment: Center
  • PlaceHolder: Value
  • Text Input Traits
  • Keyboard: Decimal Pad
  • Border Style: doted line(첫번째)

  • UILabel fontSize: 36 or 70/Bold
  • Alignment: Center

  • Textfield - background : F5F4F1
  • Label - Color : E15829

(추후에 디자이너가 정해준 색깔을 쓸때, 저렇게 HexColor쓰면댐)

여러가지 테스트 방법

  • 테스트 방법 1

  • 태스트 방법 2


    • 얘네는 키보드 코드로 짜는거아니면 수동으로해줘야함

UIViewController

안드로이드에서 Xml을 Fragment나 Activity에 연결했던것처럼

새로 코드를 만들어주자.

  • New Empty File : 아무것도없는 스위프트 파일
  • New File From Template.. : 기본 형식은 되어있는 코드

New File From Template ㄱㄱ

좀 뭐가 다양하게많음 나중에 하나씩 살펴보셈

클래스 이름 정하고, 아래 Subclass of 를 통해 상속받을 클래스 (UIViewController, UITableViewCall 등등..)

을 정할 수 있음 이대로 ㄱ

위치 알아서 정하고

하면 이렇게 잘 나옴.

  1. 스토리보드에서 ViewController 선택
  2. Identity Inspector 선택(4번째)
  3. Class에서 ConversionViewController 선택

그럼 연결된거임

이런 구조

UIViewController Lifecycle

대체로 UIViewController의 라이프 사이클은 위와 같음.

조금 더 깊게 설명하자면,


1. 초기화 단계 (Initialization)
뷰 컨트롤러 객체가 처음 생성될 때 호출되는 메서드

  • init(coder:)
    스토리보드나 XIB 파일에서 뷰 컨트롤러가 생성될 때 호출됩니다.
required init?(coder: NSCoder) {
    super.init(coder: coder)
    print("스토리보드에서 뷰 컨트롤러 초기화")
    // 여기서는 아직 뷰나 IBOutlet 등에 접근할 수 없음
}
  • init(nibName:bundle:)
    코드로 뷰 컨트롤러를 생성할 때 호출됩니다.
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    print("코드로 뷰 컨트롤러 초기화")
    // 여기서는 아직 뷰나 IBOutlet 등에 접근할 수 없음
}

2. 뷰 로딩 단계 (View Loading)
뷰 컨트롤러의 뷰가 메모리에 로드되는 단계입니다.

  • loadView()
    뷰 계층 구조를 생성하기 위해 호출됩니다. 기본 구현은 스토리보드나 XIB에서 뷰를 로드합니다.
override func loadView() {
    super.loadView()
    print("뷰 계층 구조 생성")
    
    // 코드로 뷰를 생성하는 경우
    // self.view = CustomView() // 뷰를 직접 생성하여 할당
}
  • viewDidLoad()
    뷰가 메모리에 로드된 후 한 번만 호출됩니다. 초기 설정 작업에 적합합니다.
override func viewDidLoad() {
    super.viewDidLoad()
    print("뷰가 메모리에 로드됨")
    
    // 초기화 작업 수행
    // - UI 컴포넌트 초기 설정
    // - 데이터 초기 로딩
    // - 이벤트 리스너 등록
    // - 네트워크 요청 초기화
    
    setupTableView()
    registerCells()
    configureNavigationBar()
}

3. 뷰 표시 준비 단계 (View Will Appear)
뷰가 화면에 표시되기 직전에 호출되는 메서드입니다.

  • viewWillAppear(_:)
    뷰가 화면에 표시되기 직전마다 호출됩니다. 화면 전환 시 매번 호출됩니다.
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    print("뷰가 화면에 표시되기 직전")
    
    // 화면이 나타날 때마다 수행해야 할 작업
    // - 데이터 새로고침
    // - UI 상태 업데이트
    // - 네비게이션 바 설정 변경
    // - 알림 등록
    
    refreshData()
    updateUIState()
    navigationController?.setNavigationBarHidden(false, animated: true)
    NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: .someNotification, object: nil)
}

4. 레이아웃 단계 (Layout)
뷰의 레이아웃이 결정되는 단계입니다.

  • viewWillLayoutSubviews()
    뷰의 크기가 결정되고 서브뷰의 레이아웃이 조정되기 직전에 호출됩니다.
override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    print("서브뷰 레이아웃 직전")
    
    // 레이아웃 변경 직전 작업
    // - 특정 뷰의 프레임 조정
    // - 레이아웃 제약조건 준비
    
    prepareCustomLayoutIfNeeded()
}
  • viewDidLayoutSubviews()
    서브뷰의 레이아웃이 모두 조정된 후 호출됩니다.
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    print("서브뷰 레이아웃 완료")
    
    // 레이아웃 완료 후 작업
    // - 스크롤 위치 조정
    // - 동적 크기 뷰 업데이트
    // - 컨텐츠 오프셋 설정
    
    updateScrollViewContentInset()
    adjustContentIfNeeded()
}

5. 뷰 표시 완료 단계 (View Did Appear)
뷰가 화면에 완전히 표시된 후 호출되는 메서드입니다.

  • viewDidAppear(_:)
    뷰가 화면에 완전히 표시된 후 호출됩니다.
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    print("뷰가 화면에 완전히 표시됨")
    
    // 화면이 완전히 표시된 후 작업
    // - 애니메이션 시작
    // - 타이머 시작
    // - 사용자 입력 처리 시작
    // - 추가 데이터 로딩
    
    startAnimations()
    startRefreshTimer()
    becomeFirstResponder() // 키보드 표시 등
    loadAdditionalData()
}

6. 뷰 제거 준비 단계 (View Will Disappear)
뷰가 화면에서 사라지기 직전에 호출되는 메서드입니다.

  • viewWillDisappear(_:)
    뷰가 화면에서 사라지기 직전에 호출됩니다.
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("뷰가 화면에서 사라지기 직전")
    
    // 화면이 사라지기 전 작업
    // - 키보드 숨기기
    // - 타이머 일시 정지
    // - 사용자 입력 저장
    // - 알림 해제
    
    view.endEditing(true)
    pauseTimers()
    saveUserInput()
    NotificationCenter.default.removeObserver(self, name: .someNotification, object: nil)
}

7. 뷰 제거 완료 단계 (View Did Disappear)
뷰가 화면에서 완전히 사라진 후 호출되는 메서드입니다.

  • viewDidDisappear(_:)
    뷰가 화면에서 완전히 사라진 후 호출됩니다.
override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    print("뷰가 화면에서 완전히 사라짐")
    
    // 화면이 사라진 후 작업
    // - 리소스 해제
    // - 메모리 정리
    // - 애니메이션 중단
    // - 비용이 큰 작업 중단
    
    stopAnimations()
    releaseAudioResources()
    cancelNetworkRequests()
}

8. 메모리 관리 단계 (Memory Management)
메모리 관리와 관련된 메서드들입니다.

  • didReceiveMemoryWarning()
    시스템 메모리가 부족할 때 호출됩니다.
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    print("메모리 부족 경고 발생")
    
    // 메모리 확보 작업
    // - 캐시 정리
    // - 사용하지 않는 객체 해제
    // - 이미지 캐시 정리
    
    clearImageCache()
    releaseUnusedObjects()
}
  • deinit
    뷰 컨트롤러가 메모리에서 해제될 때 호출됩니다.
deinit {
    print("뷰 컨트롤러 메모리 해제")
    
    // 최종 정리 작업
    // - 남은 옵저버 제거
    // - 타이머 정지
    // - 순환 참조 해제
    
    NotificationCenter.default.removeObserver(self)
    timer?.invalidate()
    closure = nil // 클로저에 의한 순환 참조 해제
}

9. 중요 사항

  • 호출 순서: 라이프사이클 메서드는 위에서 설명한 순서대로 호출됩니다.
  • 반복 호출: viewWillAppear, viewDidAppear, viewWillDisappear, viewDidDisappear는 화면 전환마다 반복해서 호출됩니다.
  • 한번만 호출: viewDidLoad는 뷰 컨트롤러 생성 후 한 번만 호출됩니다.
  • super 호출: 라이프사이클 메서드를 재정의할 때는 항상 super 버전을 호출해야 합니다.

10. 다양한 시나리오

  • 앱 시작 시: initloadViewviewDidLoadviewWillAppearviewWillLayoutSubviewsviewDidLayoutSubviewsviewDidAppear
  • 다른 뷰로 이동 시 (현재 뷰 기준): viewWillDisappearviewDidDisappear
  • 이전 뷰로 돌아올 때: viewWillAppearviewWillLayoutSubviewsviewDidLayoutSubviewsviewDidAppear
  • 앱이 백그라운드로 갔다가 돌아올 때: viewWillAppearviewDidAppear
  • 앱 종료 시: viewWillDisappearviewDidDisappeardeinit

참고 :
UIViewController
UIViewController Life Cycle


라고 클로드랑 블로그가 알려줌 앱 자체의 생명주기랑은 다른거임

절대 헷갈리면안됌.

UIViewController에서 View Access

코드를 짜기전에

처음에 나올 화면을 저렇게 세팅해줘여함

  • is initial View Controller

안그럼 저기 첨에 만들었던 오른쪽화면이 나옴

-> 가 있어여함

이제 다음과 같은 코드를 넣어서 테스트해보자

class ConversionViewController: UIViewController {
   
   override func viewDidLoad() {
       super.viewDidLoad()
       // Do any additional setup after loading the view.
       
       // view.subviews[0]이 UITextField 타입인지 검사 (is 연산자 사용)
       if view.subviews[0] is UITextField {
           // 조건이 참이면 UITextField로 다운캐스팅 후 옵셔널 체이닝(?.)을 통해 text 속성에 접근
           (view.subviews[0] as? UITextField)?.text = "123"
       }
       
       // 화씨 온도(123.0)를 섭씨로 변환하는 계산식
       // 5.0/9.0 * (F - 32) 공식 사용
       let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
       
       // view.subviews[3]이 UILabel 타입인지 검사
       if view.subviews[3] is UILabel {
           // 강제 다운캐스팅(as!)을 사용해 UILabel로 변환 후 text 속성에 값 할당
           // String() 생성자를 사용해 Double 값을 문자열로 변환
           (view.subviews[3] as! UILabel).text = String(celsiusValue)
       }
   }
}

오른쪽 스토리보드의 Safe Area 밑에 View들은 사실 배열의 나열이다.

이런식으로

중요하게 볼 포인트는

  • viewDidLoad()

    뷰 컨트롤러의 뷰가 메모리에 로드된 직후 호출되는 메서드
    UI 초기화와 데이터 설정에 적합한 위치

  • subviews 배열

    view.subviews[0]: 첫 번째 서브뷰 (화씨 온도 입력 필드)
    view.subviews[3]: 네 번째 서브뷰 (섭씨 결과를 표시할 레이블)

  • 타입 체크와 캐스팅

    is UITextField: 객체가 UITextField 타입인지 확인
    as? UITextField: 안전한 다운캐스팅 (실패 시 nil 반환)
    as! UILabel: 강제 다운캐스팅 (실패 시 런타임 오류)

  • 옵셔널 체이닝

    (view.subviews[0] as? UITextField)?.text: 캐스팅 성공 시에만 text 속성에 접근

만약, 객체의 타입이 더 명확할 땐,

강제 다운캐스팅(as!) 을 사용해주는게 더 깔끔 할 수도있음.

  • Optional Chain을 사용하여 간단하게 해보자
    • xxx?.yyy?.zzz?에서 하나라도 nil이면 전체가 nil임
class ConversionViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // if view.subviews[0] is UITextField {
        //     (view.subviews[0] as? UITextField)?.text = "123"
        // }
        // 어차피 view.subviews[0]가 UITextField이 아니라면 "123"은 나타나지 않는다.
        (view.subviews[0] as? UITextField)?.text = "123"
        
        let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
        
        // if view.subviews[3] is UILabel {
        //     (view.subviews[3] as! UILabel).text = String(celsiusValue)
        // }
        // 어차피 view.subviews[3]이 UITextField이 아니라면 "123"은 나타나지 않는다.
        (view.subviews[3] as? UILabel)?.text = String(celsiusValue)
    }
}

as! 강제 다운캐스팅을 사용해 타입 변환 후 속성에 직접 접근할 수 도 있음.

– Implicit Unrapping을 사용하여 ?를 최소화
• 명확한 것은 ?보다 !를 사용


class ConversionViewController: UIViewController {
   override func viewDidLoad() {
       super.viewDidLoad()
       // Do any additional setup after loading the view.
       
       // (view.subviews[0] as? UITextField)?.text = "123"
       // view.subviews[0]가 명확히 UITextField이기 때문에 as!를 사용
       // 따라서 두번째 ?를 제거할 수 있다.
       (view.subviews[0] as! UITextField).text = "123"
       
       let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
       
       // (view.subviews[4] as? UILabel)?.text = String(celsiusValue)
       // view.subviews[4]가 명확히 UILabel이기 때문에 as!를 사용
       // 따라서 두번째 ?를 제거할 수 있다.
       (view.subviews[4] as! UILabel).text = String(celsiusValue)
   }
}

직접 참조를 하기에는 UView가 생길때마다 계속 일일이 해줘야함
따라서,

fahrenheitTextFieldcelsiusLabel 프로퍼티를 클래스에 선언하고 초기화 후 재사용

class ConversionViewController: UIViewController {
   var fahrenheitTextField: UITextField!
   var celsiusLabel: UILabel?
   // !와 ?의 차이가 무엇인지 아래에서 파악하라
   
   override func viewDidLoad() {
       super.viewDidLoad()
       
       fahrenheitTextField = view.subviews[0] as! UITextField
       celsiusLabel = view.subviews[4] as? UILabel
       
       // (view.subviews[0] as! UITextField).text = "123"
       fahrenheitTextField.text = "123"
       
       let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
       
       // (view.subviews[4] as! UILabel).text = String(celsiusValue)
       celsiusLabel?.text = String(celsiusValue)
   }
}

근데 사실 이것도 사용 안함.

▪ 앞 방법의 문제점

  • Document Outline에서 뷰의 순서를 바꾸면 ViewController에서 배열의 인덱스 를 항상 재설정하여야 함
  • 안드로이드처럼(findViewById()) 일일이 별도의 프로그래밍을 하여야 함

▪ xcode에서 다소 편리한 기능제공

  • IBOutlet
    • 스토리보드의 UIView객체에 대하여 자동으로 ViewController의 변수와 연결, Document Outline에서 위치가 바뀌어도 문제 없음
  • IBAction
    • 스토리보드의 UIView객체의 이벤트 핸들러 함수를 자동으로 ViewController의 함수와 연결, Document Outline에서 위치가 바뀌어도 문제 없음

방법1


방법2


함수간 연결 방법

그러면 다음과 같은 화면이 보일꺼임

대부분의 이야기는 여기서 어느정도 설명했으므르 패스

  • 코드완성 :
    화씨를 섭씨로 바꾸는 함수 f(화씨) = 5.0 / 9.0 * (화씨 – 32.0)
class ConversionViewController: UIViewController {
   @IBOutlet weak var celsiusLabel: UILabel!
   @IBOutlet weak var fahrenheitTextField: UITextField!

   override func viewDidLoad() {
       super.viewDidLoad()
   }

   @IBAction func fahrenheitEditingChanged(_ sender: UITextField) {
       // 여기서 sender이 fahrenheitTextField 객체이다.
       if let text = sender.text { // 그내용이 뭔가 있으면, 옵셔널 바인딩

           // 문자열 -> Double 형변환, Double(text)가 nil수도 있다
           if let fahrenheitValue = Double(text){
               let celsiusValue = 5.0/9.0*(fahrenheitValue - 32.0) // 변환
               celsiusLabel.text = String(format: "%.2f", celsiusValue)
           }else{
               celsiusLabel.text = "???"
           }
       }
   }
}

UIView의 이벤트 핸들러

UIControl

  • 이벤트를 받아서 처리할 수 있는 UIView

    UIButton: 탭할 수 있는 버튼
    UIDatePicker: 날짜와 시간 선택기
    UIPageControl: 페이지 표시기
    UISegmentedControl: 세그먼트 컨트롤
    UITextField: 텍스트 입력 필드
    UISlider: 슬라이더
    UISwitch: 전환 스위치
    등등...

UIResponder

UIResponder 클래스

얘는 이벤트 흐름뿐만 아니라 iOS 앱에서 사용자 상호작용의 전반적인 처리를 담당하는 핵심 클래스

  • UIView의 수퍼 클래스이다. → 모든 UI객체는 이 객체의 하위 객체임

  • 터치 이벤트, 모션 이벤트, 원격 제어 이벤트, 프레스 이벤트 등 다양한 유형의 이벤트 처리

  • 응답자 체인(Responder Chain)의 일부로 작동하여 이벤트 처리의 계층 구조 형성

  • 첫 번째 응답자(First Responder) 기능 지원
    • NSObject -> UIResponder -> (UIView, UIViewController, UIApplication 등)

    UIView: 화면에 콘텐츠를 표시하는 모든 시각적 요소의 기본 클래스
    UIViewController: 뷰의 콘텐츠를 관리하는 컨트롤러
    UIApplication: 앱의 이벤트 루프를 관리하고 앱 수준의 기능 제공

Managing the Responder Chain(이벤트의 흐름)

  1. 사용자가 버튼을 터치합니다.
  2. 히트 테스트를 통해 UIButton이 첫 번째 응답자로 지정됩니다.
  3. 버튼이 터치를 처리하지 않으면, 이벤트는 상위 뷰로 전달됩니다.
  4. 계속해서 처리되지 않으면, 뷰 컨트롤러 → 윈도우 → 애플리케이션 → 앱 델리게이트 순으로 전달됩니다.

First Responder

First Responder in UIKit

  • iOS에서 현재 이벤트를 우선적으로 수신하는 객체
  • 키보드 입력, 모션 이벤트, 원격 제어 이벤트 등을 받는 대상
  • UIResponder 클래스의 인스턴스만 First Responder가 될 수 있음

▪ First Responder 지정

  • ios가 지정
  • 이것으로 지정되면 해당 View의 becomeFirstResponder을 호출

▪ First Responder의 해제

  • resignFirstResponder호출로 해제

뭔소리냐면

First Responder와 UIResponder의 관계

핵심 관계

  • First Responder는 상태(status)입니다. UIResponder는 클래스입니다.
  • First Responder가 될 수 있는 객체는 반드시 UIResponder 클래스의 인스턴스여야 합니다.
  • 모든 UIResponder는 잠재적으로 First Responder가 될 수 있지만, 특정 시점에 오직 하나의 UIResponder만 First Responder 상태가 됩니다.

관계의 구체적 설명

  1. 클래스와 상태의 관계
  • UIResponder: 이벤트를 수신하고 처리할 수 있는 능력을 가진 클래스
  • First Responder: 특정 UIResponder 객체가 가질 수 있는 특별한 상태
  1. 유추로 설명하면
  • UIResponder는 "사람"이라는 개념
  • First Responder는 "현재 발언권을 가진 사람"이라는 상태
  • 여러 사람(UIResponder)이 있지만, 특정 시점에 발언권(First Responder)을 가진 사람은 한 명뿐
  1. 기능적 관계
  • UIResponder 클래스는 First Responder 상태를 관리하는 메서드를 제공:
    • becomeFirstResponder(): First Responder 상태가 되도록 요청
    • resignFirstResponder(): First Responder 상태를 포기
  • 이 메서드들은 UIResponder 클래스에 정의되어 있으며, 모든 UIResponder 인스턴스에서 사용 가능
  1. 이벤트 처리 관점에서
  • UIResponder: 이벤트를 처리할 수 있는 능력을 가진 객체
  • First Responder: 현재 이벤트를 우선적으로 수신하는 UIResponder 객체

요약하면, First Responder는 UIResponder의 특별한 상태이며, 이 상태를 가진 UIResponder가 이벤트를 최초로 수신하는 권한을 갖습니다. 이는 마치 여러 사람(UIResponder) 중에서 현재 발언권(First Responder)을 가진 한 사람이 우선적으로 발언(이벤트 수신)할 수 있는 것과 같습니다.

라고 클로드가 알려줌

키보드 숨기기

마지막으로, IOS는 안드로이드 애뮬레이터마냥
알아서 키보드가 생기고 그러지않음 개쓰레기같은거

그래서 View에 Tap 제스처로 해결해야만함

– TextField외 어느 곳이든 클릭(Tapping)하면 호출되는 함수 지정
– UITapGestureRecognizer 객체 생성 및 view에 부착

IOS에서의 Delegation

  • 개념 정의

Delegation: 특정 객체가 해야 하는 일을 대행하는 위임자(delegate)를 지정하는 디자인 패턴

목적: 객체 간 결합도를 낮추고 코드의 재사용성을 높임

구조: 하나의 delegate는 여러 개의 callback 함수를 포함할 수 있음

  • 동작 방식
  1. 위임 관계 설정

    위임하는 객체(Delegator)는 delegate 속성을 통해 위임받는 객체를 참조
    delegate가 nil이면 위임하는 객체는 관련 이벤트에 대해 아무 동작도 수행하지 않음

  1. 프로토콜 정의

    위임 관계는 프로토콜(Swift/Objective-C)로 정의됨
    이 프로토콜은 위임받는 객체가 구현해야 할 메서드들을 선언

  1. 이벤트 처리

    위임하는 객체에서 이벤트 발생 시, delegate의 적절한 메서드를 호출
    위임받는 객체는 호출된 메서드에서 필요한 작업을 수행

EX : UITextFieldDelegate 프로토콜
– 프로토콜은 자바에서 인터페이스와 같다

만드는법

– 아래 그림과 같이 별도의 extension에서 UITextFieldDelegate를 상속받는다.
– extension 내부({ })에서 textField를 타이핑하면 관련 딜리게이터 함수에 대한 팝 업창이 뜬다.
– 그곳에서 원하는 릴리게이터 함수를 선택한다.

참고

완성 코드

//
//  ConversionViewController.swift

import UIKit

class ConversionViewController: UIViewController {
    @IBOutlet weak var fahrenheitTextField: UITextField!
    
    @IBOutlet weak var celsiusLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        // – UITapGestureRecognizer 객체 생성 및 view에 부착
        view.addGestureRecognizer(tapGesture)

        //Delegator 지정
        fahrenheitTextField.delegate = self
    }
    

    
    @IBAction func fahrenheitEditingChanged(_ sender: UITextField) {
        if let text = sender.text { // 그내용이 뭔가 있으면, 옵셔널 바인딩

          // 문자열 -> Double 형변환, Double(text)가 nil
            if let fahrenheitValue = Double(text){
                            let celsiusValue = 5.0/9.0*(fahrenheitValue - 32.0) // 변환
                            celsiusLabel.text = String(format: "%.2f", celsiusValue)
            }else{
                celsiusLabel.text = "???"
            }
        }
    }
    
}


// TextField외 어느 곳이든 클릭(Tapping)하면 호출되는 함수 지정
extension ConversionViewController{
    @objc func dismissKeyboard(sender: UITapGestureRecognizer){
        fahrenheitTextField.resignFirstResponder()
    }
}
// UITextFieldDelegate 생성
extension ConversionViewController: UITextFieldDelegate{
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        
        let existing = textField.text?.range(of: ".")
        let comming = string.range(of: ".")
        if existing != nil, comming != nil {
            return false
        }
        return true

    }
}
profile
디지털 치매 예방

0개의 댓글