operator

Observable이 수행하는 메소드. Observable에서 파생되는 스트림을 구성하는 rxSwift의 핵심요소라고 할 수 있다. 

Observable에는 수행가능한 여러가지 operator가 있는데, 그중 대표적인것들에 대해서 적어보려고 한다. 

 

just()

static func just(_ element: [String]) -> Observable<[String]>
//returns an Observable sequence that contains a single element
Observable.just(_ element: )

입력 요소에 대해 그대로 반환되는 연산자이다. 예시를 통해 보자. 

@IBAction func exJust1() {
    Observable.just("Hello World")
        .subscribe(onNext: { str in
            print(str)
        })
        .disposed(by: disposeBag)
}
//Hello World!    출력

@IBAction func exJust2() {
    Observable.just(["Hello", "World"])
        .subscribe(onNext: { arr in
            print(arr)
        })
        .disposed(by: disposeBag)
}
//["Hello","World"]    출력

배열을 넣어도 배열이 통째로 출력되는 모습을 볼 수 있다.


from()

public static func from(_ array: [Element]) -> Observable<Element>
//Converts an array to an observable sequence.
Observable.from(_ array: [_])

배열의 원소들을 Observable의 형태로 리턴해준다.

@IBAction func exFrom1() {
    Observable
        .from(["RxSwift", "In", "4", "Hours"])
        .subscribe(onNext: { str in
            print(str)
        })
        .disposed(by: disposeBag)
}
//RxSwift
//In
//4
//Hours 출력

from을 통해 나오는 Observable<String>형태의 원소들이 subscribe(OnNext: )부분에서 출력이 된다.


map()

func map<Result>(_ transform: @escaping (String) throws -> Result) -> Observable<Result>
//Projects each element of an observable sequence into a new form.

swift에서의 map()과 같은 동작을 하는 operator이다. Observable에서 생성되어 내려오는 데이터들에 closure에 적힌 동작에 따라 가공하여 리턴해준다. 

@IBAction func exMap1() {
    Observable.just("Hello")
        .map { str in "\(str) RxSwift" }
        .subscribe(onNext: { str in
            print(str)
        })
        .disposed(by: disposeBag)
}
//Hello RxSwift 출력

@IBAction func exMap2() {
    Observable.from(["RxSwift", "ReactiveX"])
        .map { $0.count }
        .subscribe(onNext: { str in
            print(str)
        })
        .disposed(by: disposeBag)
}
//7
//9 출력

아마도 가장 많이 쓰게될 operator가 아닐까 생각된다.


filter()

func filter(_ predicate: @escaping (Int) throws -> Bool) -> Observable<Int>
//Filters the elements of an observable sequence based on a predicate.

swift의 filter()함수와 동일하게 동작하는 오퍼레이터이다. 서술한 조건에 맞는 경우에만 Observable을 내보낸다.

@IBAction func exFilter() {
    Observable.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
        .filter { $0 % 2 == 0 }
        .subscribe(onNext: { n in
            print(n)
        })
        .disposed(by: disposeBag)
}
//2
//4
//6
//8
//10 출력

배열요소들 중에 짝수인 요소만 출력된다.


stream

우리는 이러한 여러가지 오퍼레이터들을 통해 Observable을 가공하여 비동기 프로그래밍을 할 수 있게 된다. 앞의 글에서도 설명했지만 이러한 가공의 과정을 stream이라 부르기로 했고, 이러한 Observable을 통한 stream을 통해 비동기 프로그래밍을 할 수 있게 해주는 것이 RxSwift이다.

@IBAction func exMap3() {
    Observable.just("800x600")
        .map { $0.replacingOccurrences(of: "x", with: "/") }    //"800/600"
        .map { "https://picsum.photos/\($0)/?random" }          //"https://picsum.photos/\800/600/?random"
        .map { URL(string: $0) }                                //URL?
        .filter { $0 != nil }
        .map { $0! }                                            //URL!
        .map { try Data(contentsOf: $0) }                       //Data
        .map { UIImage(data: $0) }                              //UIImage?
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
        .disposed(by: disposeBag)
}

Observable이 여러가지 오퍼레이터를 통해 어떤식으로 변화하는지 주석으로 적어놓았다. 

 

이외에도 정말 많은 오퍼레이터들이 있으며, 사용처에 따라 알맞은 오퍼레이터를 사용하면 된다. 각 오퍼레이터에 대한 상세한 내용들도 홈페이지에서 열람 가능하다.

 

[참고영상]

[유튜브] 곰튀김 RxSwift 4시간에 끝내기 - 4

[유튜브] 곰튀김 RxSwift 4시간에 끝내기 - 5

'iOS > RxSwift' 카테고리의 다른 글

scheduler  (0) 2022.08.31
next, error, complete  (0) 2022.08.19
subscribe, dispose  (0) 2022.06.15
Observable stream  (0) 2022.06.08
ReactiveX  (0) 2022.05.30

자신의 앱에 구글 애드몹을 사용한다면, 추적광고 때문에 반드시 사용자에게 해당 알림을 띄워야만 앱심사에서 통과된다. 일반적인 경우 앱의 최초실행에만 알림이 뜨면 되는 터라, 앱 출시 당시 정상 동작하는것만 확인하고서는 그뒤로는 잘 되겠거니 하고서 따로 확인을 하지 않았다.

이후 아이폰의 iOS업데이트의 영향인지 혹은 내가 변경한 코드들의 영향때문인지는 모르겠으나 일반적인 호출로는 알림이 뜨지않아 IDFA를 가져오는 과정에서 "Not Determined" 상태가 리턴되는 문제가 발생하였다. 

func requestPermission() {
    ATTrackingManager.requestTrackingAuthorization { status in
        switch status {
        case .authorized:
            // Tracking authorization dialog was shown
            // and we are authorized
            print("[googleAdmob]: Authorized")
            // Now that we are authorized we can get the IDFA
            print(ASIdentifierManager.shared().advertisingIdentifier)
        case .denied:
            // Tracking authorization dialog was
            // shown and permission is denied
            print("[googleAdmob]: Denied")
        case .notDetermined:
            // Tracking authorization dialog has not been shown
            print("[googleAdmob]: Not Determined")
        case .restricted: print("[googleAdmob]: Restricted")
        @unknown default: print("[googleAdmob]: Unknown")
        }
    }
}

앱추적 권한 상태는 크게 4가지. Authorized(허용됨), denied(거부됨), NotDetermined(결정안됨), restricted(제한됨)

주석들을 보면 알겠지만 추적권한 알림이 뜨지 않는 상태라는 걸 알수있다. 해결법은 StackOverFlow에서 손쉽게 찾을수 있었다. 

DispatchQueue.main.asyncAfter(deadline: .now()+0.2){
    self.requestPermission()
}

requestPermission() 함수를 호출하던 부분을 비동기 방식, 예약호출을 통해 실행하니 해결되었다. 현재시간에서 0.01초 단위로 늘려보았고, 0.13초부터 알림이 뜨기시작했다. 혹시 모르니 0.2초 예약호출로 작성하였고, "Not Determined"문제는 이렇게 해결되었다.

subscribe

비동기 요청을 위한 stream을 생성하기 위해 Observable을 보면 여러가지 operator가 존재하는 걸 알 수 있는데, 그중 가장 중요한 subscribe()와 dispose()를 알아보자. 

지난번 Rxswift로 작성된 코드를 보면 Subscribe()와 disposed()로 마무리가 되어있는걸 확인 할 수 있다. 

var disposebag = DisposeBag()
@IBAction func onLoadRxAsync(_ sender: Any) {
    Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
        .disposed(by: disposebag)
}

그렇다면 subscribe는 무엇일까? 

func subscribe(onNext: ((_) -> Void)? = nil,
	onError: ((Error) -> Void)? = nil, 
	onCompleted: (() -> Void)? = nil, 
	onDisposed: (() -> Void)? = nil) -> Disposable
The Subscribe operator is the glue that connects an observer to an Observable. In order for an observer to see the items being emitted by an Observable, or to receive error or completed notifications from the Observable, it must first subscribe to that Observable with this operator.

Observerble과 Observer사이를 연결해주는 operator라고 한다. Observer와 연결하게 되면서 Observable이 아이템을 방출하거나, 에러 혹은 완료를 알림받게 된다. 즉 Observerble이 발생시키는 이벤트들을 확인할 수 있는 operator인 것이다. 

 

Disposable

그렇다면 subscribe()의 리턴값인 Disposable은 무엇일까? 

Observable에서 구독(subscribe)받는 이벤트들은 처분(dispose)할 시점이 반드시 오게된다. 즉 Disposable은 dispose가능한 자료형의 형태로 보면된다. (먼저 구독을 해야 처분을 받을수 있으니)

이 얘기는 subscribe를 통해 작업이 일어나는 도중, 임의의 시점에 강제로 처분(dispose)이 가능하다는 얘기이다.

@IBAction func onLoadRxAsync(_ sender: Any) {
    self.imageView.image = nil
        
    let disposable = Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
}

즉 예시와 같이 처분가능한(disposable)형태로 담아둘수 있다는 이야기이고, 이를 전역변수에 담아두게된다면 다른 함수호출을 통해 요청취소가 가능하다는 이야기이다. 

var disposable: Disposable?
@IBAction func onLoadRxAsync(_ sender: Any) {
    self.imageView.image = nil
        
    disposable = Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
}

 

Disposebag()

여러개의 Disposable을 처리하기 위해 Disposablebag을 활용할 수 있다.

var disposebag = DisposeBag()
    
@IBAction func onLoadRxAsync(_ sender: Any) {
    self.imageView.image = nil
        
    let disposable = Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
    disposebag.insert(disposable)
}

insert메소드를 통해 추가가 가능하며, 예시처럼 단일형태의 Disposable도 가능하지만, 배열형태나, 범위의 형태로도 추가 가능하다.

추가로 같은 동작을 조금 더 간결하게 작성하는 것도 가능하다. disposable에 속해있는 disposed(by:) 메소드를 사용하면 자기자신을 Disposablebag에 추가한다.

func disposed(by bag: DisposeBag){
    //Adds self to bag
    //bag: DisposeBag to add self to.
}
var disposebag = DisposeBag()
    
@IBAction func onLoadRxAsync(_ sender: Any) {
    self.imageView.image = nil
        
    Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
        .disposed(by: disposebag)
}

그렇게 처음에 적힌 예시코드와 같은 형태로 완성이 된다. 

var disposebag = DisposeBag()
    
@IBAction func onLoadRxAsync(_ sender: Any) {
    self.imageView.image = nil
        
    Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
        .disposed(by: disposebag)
}
@IBAction func cancelRxAsync(_ sender: Any) {
    disposebag = DisposeBag()
}

Disposebag을 사용하여 작업을 취소하는 방법은 간단하다. 원하는 시점에 Disposebag을 초기화만 해주면 된다. 

취소버튼을 통해 작업을 취소하는 모습을 확인 할 수 있다.

 

참고영상

[유튜브] 곰튀김 RxSwift 4시간에 끝내기 - 3

 

'iOS > RxSwift' 카테고리의 다른 글

scheduler  (0) 2022.08.31
next, error, complete  (0) 2022.08.19
operator  (0) 2022.07.21
Observable stream  (0) 2022.06.08
ReactiveX  (0) 2022.05.30

 

sync, async

Rx에서 가장 중요한 개념인 Observable에 대해 이야기하기전에 우선 동기(sync)와 비동기(async) 프로그래밍에 대해 간단하게 설명하고자 한다. 

private func loadImage(from imageUrl: String) -> UIImage? {
        guard let url = URL(string: imageUrl) else { return nil }
        guard let data = try? Data(contentsOf: url) else { return nil }
        
        let image = UIImage(data: data)
        return image
}

 

여기 URL요청을 통해 이미지를 불러와 UIImage 형태로 리턴해주는 함수가 있다. 동기방식으로 처리할경우 이미지를 요청하고 요청에 대한 응답을 받아야만 다음처리가 가능해진다. 이에 반해 비동기방식은 요청을 하고난 후 응답을 받기 전에 다음 작업들을 이어서 처리하다가 요청에 대한 응답이 수신되면 응답에 대한 처리를 마저 하게되는 방식이다.  동기방식은 요청에 대한 응답이 올때 까지 대기. 비동기방식은 요청에 대한 응답을 대기하지 않고 다음작업들을 처리. 차이라고 보면 된다. 아래의 코드를 통해 두 방법의 차이를 확인해보자.

 

//동기방식
@IBAction func onLoadSync(_ sender: Any) {
    let image = loadImage(from: IMAGE_URL)
    imageView.image = image
}

//비동기방식
@IBAction func onLoadAsync(_ sender: Any) {
    DispatchQueue.global().async {
        let image = self.loadImage(from: self.IMAGE_URL)
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }
}

 

우측의 시간카운터를 보면 동기방식과 비동기방식의 차이를 확인할 수 있다. 

 

동기방식으로 요청을 처리할경우, 응답을 받을 때까지 아무런 작업을 할 수 없게된다. 이렇게 동기방식으로 작성된 앱은 사용자에게 큰 불편을 유발하게 된다. 하지만 위의 예시처럼 비동기 방식으로 작성하게되면 코드의 가독성이 떨어진다.

Observable stream

다음은 RxSwift로 작성된 비동기 코드를 보자.

var disposebag = DisposeBag()
@IBAction func onLoadRxAsync(_ sender: Any) {
    Observable.just(self.IMAGE_URL)
        .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
        .map{self.loadImage(from: $0)}
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { image in
            self.imageView.image = image
        })
        .disposed(by: disposebag)
}

처음 Rx코드를 보았을때 오히려 코드라인이 늘어나서 당황했다. 하지만 코드의 흐름을 보면 들여쓰기와 중괄호가 아닌 Observable에서 시작되는 메소드들로 흘러가는 모양을 볼 수 있고, 마지막 subscribe부분에서 UI처리를 하는 모습을 볼 수 있다. 이러한 형태가 바로 이전글에 Rx의 정의를 이해할 수 있는 좋은 예시이다.

An API for asynchronous programming
with "observable streams"

Observable에서 파생되는 stream(흐름)을 통한 비동기 프로그래밍 API. 즉 Observable에서 시작되는 반환값이 여러가지 Operator, Subject, Scheduler들로 이루어진 흐름을 통해 비동기 프로그래밍을 할 수 있는 API라고 할 수 있다. 이것이 Rx의 정의이다.

 

참고영상

[유튜브] 곰튀김 RxSwift 4시간에 끝내기 - 2

'iOS > RxSwift' 카테고리의 다른 글

scheduler  (0) 2022.08.31
next, error, complete  (0) 2022.08.19
operator  (0) 2022.07.21
subscribe, dispose  (0) 2022.06.15
ReactiveX  (0) 2022.05.30

 

 

RxSwfit. 여러 개발 블로그를 돌아다니다보면, 기술스택에 RxSwift가 적혀있는 것을 심심치않게 확인해 볼 수 있다. 볼때마다 저건 무엇일까 라는 의문만 남긴채 지나갔지만, 이번기회에 유튜브에서 곰튀김님의 "RxSwift 4시간에 끝내기" 영상을 보면서 공부한 내용을 적으려고 한다. 

What is Rx?

RxSwift는 ReactiveX의 Swift버전이라고 생각하면 된다. 그렇다면 reactiveX는 무엇일까? 공식 홈페이지에 들어가면 바로 확인이 가능하다.

An API for asynchronous programming
with observable streams

API이다. 그것도 비동기 프로그래밍을 위한. 시각적인 스트림을 통해서. 처음에는 스트림이 무엇인지 몰라 감이 안잡혔지만, 어찌되었든 결론은 ReactiveX는 비동기 프로그래밍에 사용되는 API라는 것을 알게 되었다. 

ReactiveX(이하 Rx)는 MS에서 모종의 프로젝트에 적용하기 위해 개발이 되었고, Rx를 통해 프로젝트를 진행해보니 좋았다는 피드백을 통해 여러가지 언어들에 맞게 변형되어 사용되게 된다. 홈페이지에 보면 C, java, python부터 시작하여 현대에 쓰이는 많은 언어들을 지원하는 모습을 확인 할 수 있다.

What is in Rx?

Rx에는 크게 다섯가지 요소가 있다. Observable, Operators, Scheduler, Subject, Single 이다. 서론 글을 짧게 마치고, 각 요소들에 대한 설명을 다음 글부터 이어가도록 하겠다.

 

참고영상

[유튜브] 곰튀김 RxSwift 4시간에 끝내기 - 1

'iOS > RxSwift' 카테고리의 다른 글

scheduler  (0) 2022.08.31
next, error, complete  (0) 2022.08.19
operator  (0) 2022.07.21
subscribe, dispose  (0) 2022.06.15
Observable stream  (0) 2022.06.08

지난번에 이어서 PlayingCard 데모를 작성했다.

이번에는 PlayingCard, PlayingCardDeck 모델에 이어서 PlayingCardView ViewController를 작성하였다.


cornerLabel

가장 먼저 작성한 부분은 카드의 모서리에 문양(suit)과 숫자(rank)를 적는 것이었다.

private func centeredAttributedString(_ string:String, fontSize:CGFloat) -> NSAttributedString{
    //매개변수로 들어온 폰트 크기의 본문폰트 생성
    var font = UIFont.preferredFont(forTextStyle: .body).withSize(fontSize)
    //사용자 맞춤 폰트 크기로 변환
    font = UIFontMetrics(forTextStyle: .body).scaledFont(for: font)
        
    //문단스타일에 가운데 정렬 설정
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.alignment = .center
        
    //매개변수로 들어온 문자열과, 문단스타일, 폰트를 적용하여 NSAttributedString 리턴
    return NSAttributedString(string: string, attributes: [.paragraphStyle: paragraphStyle, .font: font])
}
    
private var cornerString: NSAttributedString{
    return centeredAttributedString(rankString+"\n"+suit, fontSize: cornerFontSize)
}
    
private func createCornerLabel() -> UILabel{
    let label = UILabel()
    label.numberOfLines = 0 //레이블의 줄 수가 0이면 무제한이 된다. (쓰는만큼 늘어남)
    addSubview(label)   //해당 레이블을 서브뷰로 추가한다.
    return label
}
    
private lazy var upperLeftCornerLabel: UILabel = createCornerLabel()
private lazy var lowerRightCornerLabel: UILabel = createCornerLabel()
    
//레이블 내용 및 사이즈 변경 함수
private func configureCornerLabel(_ label: UILabel){
    //레이블의 텍스트 내용 변경
    label.attributedText = cornerString
    //sizeToFit을 하기 전에 기존의 크기정보를 초기화. *sizeToFit()호출시 width가 따로 지정되어 있는 상태라면 width는 그대로 둔상태로 높이만 줄이기에 너비도 딱맞게 줄이고 싶다면 width의 값을 0으로 만들어야한다.
    label.frame.size = CGSize.zero
    //label 사이즈를 내용에 맞게 줄임
    label.sizeToFit()
        
    //카드가 앞면상태가 아니라면 isHidden으로 레이블을 가린다.
    label.isHidden = !isfaceUp
}

첫번째로 rank와 suit를 가지고서 문자열을 생성. 그리고 UILabel을 playingCardView에 서브뷰로 추가하는 작업이다. 일반적인 트럼프카드를 생각해보면 알수있듯이 카드의 문양과 숫자는 좌측상단과 우측하단에 위치한다. 따라서 UILabel을 두개를 생성하여 서브뷰로 추가하였다. 이후 생성된 서브뷰의 내용을 rank와 suit가 담긴 문자열로 변경 및 알맞은 사이즈로 수정하는 configureCornerLabel()함수를 생성하였다.


traitCollectionDidChange()

화면회전 및 폰트크기변경과 같은 뷰의 특성에 변화가 일어나면 자동으로 수행되는 함수이다.

//화면회전, 폰트크기변경 등의 특성에 변경이 일어나면 수행되는 함수
//글자크기의 변경이 일어났을 때, 이러한 변경을 바로 반영하기위해 오버라이딩하였다.
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    //폰트크기의 변화가 일어나면 바로 드로잉과 배치를 다시 시작하여 변경사항을 반영한다.
    setNeedsDisplay()
    setNeedsLayout()
}

화면의 회전 또는 사용자의 텍스트 크기변경 등 뷰에 변화가 일어날 때마다 자동으로 뷰를 재배치 및 드로잉이 다시 수행되도록 traitCollectionDidChange()함수를 오버라이딩 하였다. 이때 호출해야할 함수는 draw()와 layoutSubViews를 대신하여 호출하는 setNeedsDisplay()와 setNeedsLayout()함수이다.    *draw()함수와 layoutSubView()는 swift만이 호출하는 함수이기 때문이다.


layoutSubView()

뷰의 배치를 담당하는 함수이다. swift에 의해서만 호출이 된다. 

//서브뷰 배치 함수
override func layoutSubviews() {
    super.layoutSubviews()
    //레이블 내용 다시 그리기
    configureCornerLabel(upperLeftCornerLabel)
    //좌측상단 레이블 재배치
    upperLeftCornerLabel.frame.origin = bounds.origin.offsetBy(dx: cornerOffset, dy: cornerOffset)
        
    //우측하단 레비을 재배치
    configureCornerLabel(lowerRightCornerLabel)
    //affine변환 크기, 평행이동, 회전을 나타내는 변환. 비트단위 변환
    //회전의 경우 라디안(파이)를 단위를 이용한다.
    //좌측상단의 원점으로 회전을 하는 것이기에, 우리가 원하는 위치에 놓으려면 카드의 사이즈만큼 평행이동 후에 회전을 시켜야한다.
    lowerRightCornerLabel.transform = CGAffineTransform.identity
        .translatedBy(x: lowerRightCornerLabel.frame.size.width, y: lowerRightCornerLabel.frame.size.height)
        .rotated(by: CGFloat.pi)
    lowerRightCornerLabel.frame.origin = CGPoint(x: bounds.maxX, y: bounds.maxY)
        .offsetBy(dx: -cornerOffset, dy: -cornerOffset)
        .offsetBy(dx: -lowerRightCornerLabel.frame.size.width, dy: -lowerRightCornerLabel.frame.size.height)
}

뷰의 레이아웃을 담당하는 layoutSubView()함수를 오버라이딩 하였다. super.layoutSubViews()가 호출된 이후에 수행할 동작들을 작성하면된다. 여기서 주목해야할 점은 좌측상단의 UILabel은 지정된 offSet(여백) 만큼만 뷰의 원점 좌표에서 이동을 하면 되지만, 우측하단의 UILabel의 경우, 180도 회전과 카드크기만큼의 평행이동이 이루어져야 한다.

아무 생각없이 offSet만큼 이동을 한뒤에 회전을 하게되면 사진과 같이 의도한 위치에 놓일 수 없게된다.

따라서 기존의 offSet만큼 이동한 위치에서 다시 한번 카드의 크기만큼 평행이동한 뒤에 원점을 기준으로 180도 회전을 해야한다.


draw( _ rect: CGRect)

화면의 드로잉을 위한 함수이다. UIView를 상속받는 view를 생성했다면 오버라이딩하여 구현하면 된다. 오로지 swift에 의해서만 호출이 되는 함수이다. 앞글에서 이야기했지만 draw를 통한 화면드로잉에는 크게 두가지 방법이있다. context를 이용하는 방법과 UIBezierPath()를 이용하는 방법이다. 

override func draw(_ rect: CGRect){
    //contect사용방식
    if let context = UIGraphicsGetCurrentContext(){
        context.addArc(center: CGPoint(x: bounds.midX, y: bounds.midY), radius: 100.0,
                       startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        //startAngle, endAngle은 0~2라디안(파이)로 표현된다.
        context.setLineWidth(5.0)
        UIColor.green.setFill()
        UIColor.red.setStroke()

        //드로잉 컨텍스트의 경우 경로를 없애가면서 그린다. 따라서 먼저 수행된 strokePath가 앞에 선언된 경로를 지웠기에
        //fillPath에는 아무런 경로가 없어 드로잉을 하지 않게된다.
        context.strokePath()
        context.fillPath()
    }
		
    //UIBezierPath()사용방식
    let path = UIBezierPath()
    path.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: 100.0, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
    path.lineWidth = 5.0
    UIColor.green.setFill()
    UIColor.red.setStroke()
    //UIBezierPath의 경우는 컨텍스트방식과 다르다. 경로를 남긴채로 드로잉 하기때문에 겹처서 드로잉 하는것이 가능하다.
    path.stroke()
    path.fill()
}

두 방식 모두 사용방법은 같다. 드로잉할 경로를 설정한 뒤, stroke() 또는 fill()등 화면에 드로잉하는 함수를 호출하여 경로대로 그려나간다. 하지만 context방식은 경로를 따라 그리면서 이전의 경로는 지워지게 되게되어 같은 경로를 재사용할수 없는 반면, UIBezierPath()의 경우에는 같은 경로를 여러반 사용이 가능해진다. 따라서 같은 비슷한 경로를 여러번 그리는 상황이라면 UIBezierPath방법이 조금 더 손쉬울 것이다.

 

PlayingCardView.swift에서 가장 핵심함수인 draw(_ rect: CGRect)부터 보면 다음과 같다. 

override func draw(_ rect: CGRect) {
    //둥근모서리 모양 생성
    let roundedRect = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
    //설정한 모양에 맞게 클리핑
    roundedRect.addClip()
    //채우기 색상 흰색으로 설정
    UIColor.white.setFill()
    //채우기 수행
    roundedRect.fill()
        
    //카드의 뒷면 유무에 맞게 동작
    if isfaceUp{
        //카드이미지 생성. rankString+suit 형태의 이름의 이미지를 가져온다.
        //in: Bundle(for: self.classForCoder),compatibleWith: traitCollection 해당문구는 인터페이스빌더에 실시간 반영을 위해 추가한 부분이다.
        if let faceCradImage = UIImage(named: rankString+suit,in: Bundle(for: self.classForCoder),compatibleWith: traitCollection){
            //해당하는 카드의 이미지가 있다면,해당 이미지를 알맞은 비율로 축소하여 그려낸다. 0.75로 설정되어있다. 원본크기의 75% 크기이다.
            //이미지가 카드뷰보다 크면 안되기에..
            //faceCradImage.draw(in: bounds.zoom(by: SizeRatio.faceCardImageSizeToBoundsSize))
            //핀치 줌을 사용하기 위해 상수가 아닌 변수형태로 적용하기 위해 faceCardScale변수를 만들어서 코드를 수정했다.
            faceCradImage.draw(in: bounds.zoom(by: faceCardScale))
        }else{
            //이미지가 없는 카드라면 문양 드로잉
            drawPips()
        }
    }else{
        if let cardBackImage = UIImage(named: "cardback",in: Bundle(for: self.classForCoder),compatibleWith: traitCollection){
            cardBackImage.draw(in: bounds)
        }
    }
}

가장 첫번째로 직사각형의 뷰를 카드모양에 맞게 그려내는 것이다. UIBezierPath(roundedRect: , cronerRadius: )함수는 모서리의 곡률를 설정하여 모서리가 둥근 모양의 뷰를 드로잉 할 수 있다. 여기서의 곡률은 CGFloat형태의 값을 입력받게 되는데, 코드의 유지보수를 위해 상수로 설정하여 값을 입력하였다. 

카드모양의 윤곽선을 그려낸 후, 카드의 뒷면을 드로잉할지, 앞면을 드로잉 하게될지 isFaceUp의 값에 따라서 결정하게 된다. 카드의 뒷면, J, Q, K, A에 해당하는 카드의 경우, Assets 디렉토리안에 있는 이미지를 통해 불러오게 되고, 문양과 숫자로 표현되는 카드들은 drawPips()함수를 통해 숫자의 갯수에 해당하는 배열로 문양을 드로잉하게된다. 

private func drawPips(){
    //카드의 숫자를 인덱스로 삼아 해당하는 배열을 2차원 배열로 표현하였다.
    let pipsPerRowForRank = [[0],[1],[1,1],[1,1,1],[2,2],[2,1,2],[2,2,2],[2,1,2,2],[2,2,2,2],[2,2,1,2,2],[2,2,2,2,2]]
        
    func createPipString(thatFits pipRect: CGRect) -> NSAttributedString {
        let maxVerticalPipCount = CGFloat(pipsPerRowForRank.reduce(0) { max($1.count, $0) })
        let maxHorizontalPipCount = CGFloat(pipsPerRowForRank.reduce(0) { max($1.max() ?? 0, $0) })
        let verticalPipRowSpacing = pipRect.size.height / maxVerticalPipCount
        let attemptedPipString = centeredAttributedString(suit, fontSize: verticalPipRowSpacing)
        let probablyOkayPipStringFontSize = verticalPipRowSpacing / (attemptedPipString.size().height / verticalPipRowSpacing)
        let probablyOkayPipString = centeredAttributedString(suit, fontSize: probablyOkayPipStringFontSize)
        if probablyOkayPipString.size().width > pipRect.size.width / maxHorizontalPipCount {
            return centeredAttributedString(suit, fontSize: probablyOkayPipStringFontSize / (probablyOkayPipString.size().width / (pipRect.size.width / maxHorizontalPipCount)))
        } else {
            return probablyOkayPipString
        }
    }
        
    if pipsPerRowForRank.indices.contains(rank) {
        let pipsPerRow = pipsPerRowForRank[rank]
        var pipRect = bounds.insetBy(dx: cornerOffset, dy: cornerOffset).insetBy(dx: cornerString.size().width, dy: cornerString.size().height / 2)
        let pipString = createPipString(thatFits: pipRect)
        let pipRowSpacing = pipRect.size.height / CGFloat(pipsPerRow.count)
        pipRect.size.height = pipString.size().height
        pipRect.origin.y += (pipRowSpacing - pipRect.size.height) / 2
        for pipCount in pipsPerRow {
            switch pipCount {
            case 1:
                pipString.draw(in: pipRect)
            case 2:
                pipString.draw(in: pipRect.leftHalf)
                pipString.draw(in: pipRect.rightHalf)
            default:
                break
            }
            pipRect.origin.y += pipRowSpacing
        }
    }
}

참고해야할 점은 수업에서 직접 코드를 작성하지 않고 미리 작성한 코드를 복사한 터라 별도의 설명은 넘어갔는데, 눈에 띄었던 부분이 바로 드로잉 하야할 카드 문양의 배열을 2차원 배열을 통해 구현해놓았다는 점이다. 


@IBDesignable

뷰가 올바르게 동작하는지 확인하기 위해서는 계속해서 빌드를 하는 방법도 있지만, 인터페이스 빌더를 통해서도 확인이 가능하다. PlayingCardView 선언부분에 @IBDesignable이라는 키워드만 붙여주게되면 인터페이스빌더를 통해 바로 확인이 가능해지게 되는데, 해당 키워드 추가후, 인터페이스 빌더를 보면 Designable 항목이 추가된것을 확인할 수 있다. 해당 항목은 @IBInspectable 키워드가 붙어있는 변수들을 반영하게 되고, 이를 통해서 즉각적으로 확인이 가능하게 된다.

//현재 뷰의 모습을 인터페이스빌더에서 바로 확인이 가능하다. 단, UIImage(named)는 제대로 동작하지 않으니 참고할것.
//in: Bundle(for: self.classForCoder),compatibleWith: traitCollection 해당문구를 UIImage(named)함수에 추가하면,
//인터페이스빌더에 실시간 반영이 가능하다.
@IBDesignable
class PlayingCardView: UIView {
    //@IBInspectable -> 인터페이스빌더에서 해당 변수들을 추가할수있는 문구다. 이때는 타입추론이 아닌 명시적으로 자료형을 정해주어야한다.
    //카드의 문양과 숫자가 바뀌면 드로잉과 배치를 다시해야한다. 이떄 draw()함수를 직접 호출할수 없고, setNeedsDsiplay()를 호출하면 draw()가 호출된다.
    //setNeedsLayout()또한 재배치를 위한 함수이다. 우리가 직접 호출하지 못하는 layoutSubview()를 대신 호출한다.
    @IBInspectable var rank:Int = 5 { didSet {setNeedsDisplay(); setNeedsLayout()}}
    @IBInspectable var suit:String = "♥️" { didSet {setNeedsDisplay(); setNeedsLayout()}}
    @IBInspectable var isfaceUp:Bool = false { didSet {setNeedsDisplay(); setNeedsLayout()}}
    
    ...
    
}

 

해당 키워드를 사용한다면, 굳이 빌드하지 않고 인터페이스 빌더를 통해 손쉽게 동작을 확인할 수 있게 된다.


Gesture

사용자의 제스처를 감지하기 위해서는 UIGestureRecognizer 클래스를 사용해야한다. *정확히는 uiGestureRecognizer의 서브클래스들을 활용해야한다. 

 

제스처 감시는 크게 두동작으로 나뉜다.

1. UIView에 제스처감시자를 추가하는 것.     -> 주로 컨트롤러에 추가

2. 제스처를 감지했을 때 작동하는 핸들러를 작성하는 것.      -> 상황에 따라 UIView 또는 컨트롤러에 작성

 

UIGestrueRecognizer생성

@IBOutlet weak var pannableView: UIView{
	didSet{
    	let panGestureRecognizer = UIPanGestureRecognizer(
        	target: self, action : #selector(ViewController.pan(recognizer:))
        )	//감시자 생성, 생성자에 target과 action을 설정한다.
        pannableView.addGestureRecognizer(panGestrueRecognizer)	//해당 뷰에 감시자 추가
    }
}

여러 제스처 중 pan(좌우로 끄는 동작)제스처를 예시로 생성해보았다. 이용자의 제스처가 runtime에 반영이 되어야 하기때문에 didSet을 통해 감시자를 생성하게 된다. target은 제스쳐의 대상. 주로 뷰자신이나 컨트롤러를 적느다. action에는 제스처가 감지될경우 행동할 objc함수를 작성한다. 이후 해당 뷰에게 addGestureRecognizer() 함수를 호출시켜 원하는 감시자를 추가하면된다. 뷰는 여러 개의 감시자를 가질 수 있다.

 

핸들러 작성

제스처 감시자의 state 변수에 사용자의 제스처 상태가 담겨있다. 각 제스처에 해당되는 state의 상태를 switch문을 통해 동작을 정의하면 된다. 

func pan(recognizer: UIPanGestrueRecognizer){
	switch recognizer.state {
    	case .changed : fallthrough	//손을 움직이는 순간
        case .ended:	//손을 떼어낸 순간
            let translation = recognizer.translation(in: pannableView)
            recognizer.setTranslation(CGPoint.zero, in: pannableView)
        default: break	//나머지 경우
    }
}

fallthrough 키워드는 다음 case문을 동작하라는 키워드이다.

즉, pan동작의 단위 중 손이 움직이거나, 떼어낸 순간에 UIPanGestureRecognizer에 정의된 translation함수가 동작 하는 핸들러이다. 

 

UIPanGestureRecognizer에는 velocity(in: UIVIew)가 존재하며 이는 pan동작의 속도, translation()함수는 이동동작이 뷰의 어느지점에서 발생했는지 좌표를 알려주며, 

 

'iOS > Stanford' 카테고리의 다른 글

[Lecture 1] Getting Started with SwiftUI - 2  (0) 2023.06.22
[Lecture 1] Getting Started with SwiftUI - 1  (0) 2023.06.16
day05_view  (0) 2021.12.30
day04_swift_part2  (0) 2021.10.25
day03_swift_part1  (0) 2021.10.12

이번문제도 C++로 작성했던 문제를 swift로 다시 풀어보았다.

https://www.acmicpc.net/problem/1158

 

1158번: 요세푸스 문제

첫째 줄에 N과 K가 빈 칸을 사이에 두고 순서대로 주어진다. (1 ≤ K ≤ N ≤ 5,000)

www.acmicpc.net

 

풀이

문제에 적힌 방법대로 풀면 된다. 대신 기존의 배열에서 제거하는 방법이 아닌 요소를 0으로 변환하면서 배열을 순회했다. 애초에 요소는 1부터 시작하기 때문이다. 배열의 요소가 0이 아니라면 카운트를 증가하지 않고 인덱스만 증가. 카운트가 k값이 되면 그때의 요소를 요세푸스 배열에 담는 방식으로 문제를 풀어보았다.

 

풀고나서 다른 사람들의 풀이를 보니 간결하고 빠른코드가 많았다.

http://boj.kr/ef42c280ec1c4098b04deca633c2b1dc

//
//  main.swift
//  1158_swift
//
//  Created by Hyun on 2022/01/20.
//

import Foundation

let NK = readLine()!.split(separator: " ").map{Int(String($0))!}
let N = NK[0]
let K = NK[1]

var arr = Array(1...N)
var josephus = [Int]()

var cnt = 0
var idx = -1

while josephus.count < arr.count{
    cnt += 1
    idx += 1
    if idx >= arr.count{
        idx = 0
    }
    if arr[idx] == 0{
        while arr[idx] == 0{
            idx += 1
            if idx >= arr.count{
                idx = 0
            }
        }
    }
    if cnt == K{
        josephus.append(arr[idx])
        arr[idx]=0
        cnt = 0
        continue
    }
}

var text = "<"
for j in josephus{
    if j == josephus.last{
        text+=String(j)
    }else{
        text+=String(j)+", "
    }
}
text += ">"
print(text)

'Problem Solving > BOJ' 카테고리의 다른 글

[2294] 동전 2  (0) 2022.09.22
[15988] 1, 2, 3 더하기 3  (0) 2022.09.20
[1890] 점프  (0) 2022.09.17
[6588] 골드바흐의 추측  (0) 2022.09.16
[1021] 회전하는 큐  (0) 2022.01.19

알고리즘 공부를 위해 풀었던 첫 백준문제이다. c++로 풀었는데, 이틀동안 문제만 들여다 보다가 결국 답을 검색하고나서 풀게되었다.

처음에 문제를 읽고나서도 이해가 안되서 지문을 수차례 읽었던 기억이 떠오른다. 이번에 스위프트로 재작성하여 공유해본다.

 

https://www.acmicpc.net/problem/1021

 

1021번: 회전하는 큐

첫째 줄에 큐의 크기 N과 뽑아내려고 하는 수의 개수 M이 주어진다. N은 50보다 작거나 같은 자연수이고, M은 N보다 작거나 같은 자연수이다. 둘째 줄에는 지민이가 뽑아내려고 하는 수의 위치가

www.acmicpc.net

 

풀이

큐의 변화는 크게 3가지이다.

1. 큐의 가장 첫번째 원소를 제거한다. 

2. 큐의 원소를 좌측으로 회전

3. 큐의 원소를 우측으로 회전

 

당시 고민했던 부분은 회전수를 최소화하는 것인데, 좌측의 회전수가 나오면 우측의 회전수는 자연스럽게 [큐의 크기 - 좌측회전수]를 통해 알수있게 되어 둘의 크기를 비교한 뒤 값이 적은 방향으로 이동하면 된다.

http://boj.kr/138f9df0ef054fbd8dc48e1fe2274c3d

//
//  main.swift
//  1021_swift
//
//  Created by Hyun on 2022/01/19.
//

import Foundation

let NM = readLine()!.split(separator: " ").map{Int(String($0))!}
let N = NM[0]
let M = NM[1]
var q = Array(1...N)
var ans = 0

let targets = readLine()!.split(separator: " ").map{Int(String($0))!}
for target in targets{
    var left = 0
    var right = 0
    for idx in q.indices{
        if q[idx] == target{
            //회전수 계산하기
            left = idx
            right = q.count-idx
            break
        }
    }
    //회전수를 비교하여 적은 방향으로 회전하기
    if left < right{
        for _ in 0..<left{
            q.append(q.removeFirst())
            ans+=1
        }
    }else{
        for _ in 0..<right{
            q.insert(q.removeLast(), at: 0)
            ans+=1
        }
    }
    //원소 제거
    q.removeFirst()
}
print(ans)

'Problem Solving > BOJ' 카테고리의 다른 글

[2294] 동전 2  (0) 2022.09.22
[15988] 1, 2, 3 더하기 3  (0) 2022.09.20
[1890] 점프  (0) 2022.09.17
[6588] 골드바흐의 추측  (0) 2022.09.16
[1158] 요세푸스 문제  (0) 2022.01.20

+ Recent posts