티스토리 뷰

실습 내용입니다.

 

커스텀 UIView 만들기


클래스 정의를 위해 파일을 생성합니다. 

CocoaTouch나 UIKit의 서브 클레스를 생성하기 위해서는 CocoaTouch를 선택합니다.

 

파일이름은 PlayCardView하고 Subclass of는 UIView로 합니다.

 

생성 후 코드는 아래와 같습니다.

draw(rect) 함수가 주석처리가 되어있습니다.

iOS에서 해당 함수를 구현되어 있는지 확인을 합니다.

구현이 되어 있을시 따로 화면 밖에 버퍼를 생성하여 드로잉을 위한 준비를 합니다.

이러한 작업은 비용이 들기 때문에 draw 함수를 사용하지 않는다면 그냥 주석인 채로 둡니다.

draw 함수를 사용하지 않는 View에는 Stack View가 있습니다.

자신이 아닌 안에 쌓여있는 View들이 Draw를 하기 때문입니다.

 

인터페이스 빌더에서 커스텀 뷰 사용하기


위에서 생성한 PlayingCardView를 인터페이스 빌더에서 사용해봅시다.

라이브러리에서 일반 View를 드래그하여 추가합니다.

 

추가한 View를 선택한 뒤 Identity Inspector에서 class를 PlayingCardView로변경해줍니다.

 

CGContext를 사용하여 원 그리기


Draw(rect) 함수 내부에서 CGContext의 addArc 함수를 사용하여 원을 그려봅시다

addArc 함수 및 파라미터 설명입니다.

 

draw(rect) 내부에서 현재 View의 드로잉 영역은 bound 변수를 통하여 얻을 수 있습니다.

뷰 가운데에 원을 그려봅시다.

context.addArc(center: CGPoint(x: bounds.midX, y: bounds.midY),
               radius: 100.0,
               startAngle: 0,
               endAngle: 2*CGFloat.pi,
               clockwise: true)

뷰의 가운데 위치는 bounds.midX와 bounds.midY를 통하여 구할 수 있습니다.

반지름은 임의값인 100pt로 설정했습니다.

시작과 끝의 각도는 radian 값으로 0~2pi까지의 값을 가집니다.(0~360 값이 아닙니다.)

원이어야 하므로 시작은 0 끝은 2p값을 뒀습니다.(pi값은 CGFloat.pi를 통하여 구할 수 있습니다.)

그리는 방향은 원이므로 true, false 아무값이나 상관없습니다.

 

이제 선 색상과 굵기 채우기 색상을 설정해봅시다.

context.setLineWidth(5.0)
UIColor.red.setStroke()
UIColor.green.setFill()

굵기는 5.0, 선 색상은 빨강, 채우기 색상은 초록으로 지정했습니다.

 

이제 선을 그리고 채우는 코드를 추가해봅시다.

context.strokePath()
context.fillPath()

 

현재까지 draw 함수의 전체 코드는 아래와 같습니다.

class PlayingCardView: UIView {
    override func draw(_ rect: CGRect) {
        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)
            
            context.setLineWidth(5.0)
            UIColor.red.setStroke()
            UIColor.green.setFill()
            context.strokePath()
            context.fillPath()
        }
    }
}

 

위 코드를 실행을 해보면 초록색으로 지정한 채우기 색상이 나타나지 않습니다.

채우기가 적용되어 있지 않습니다.

선을 그릴시 컨텍스트에서 그릴 경로를 없애면서 그립니다.

즉, 그리기가 완료되면 선을 그릴 정보가 사라지며Fill 동작시 그릴 경로가 없기 때문에 채울 영역이 없게 됩니다.

채우기 위해선 다시 경로를 설정해야 합니다.

class PlayingCardView: UIView {
    override func draw(_ rect: CGRect) {
        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)
            context.setLineWidth(5.0)
            UIColor.red.setStroke()
            context.strokePath()
            
            context.addArc(center: CGPoint(x: bounds.midX, y: bounds.midY),
                           radius: 100.0,
                           startAngle: 0,
                           endAngle: 2*CGFloat.pi,
                           clockwise: true)
            UIColor.green.setFill()
            context.fillPath()
        }
    }
}

코드를 실행해보면 정상적으로 fill이 적용되어 있습니다.

 

UIBezierPath를 사용하여 원 그리기


이번에는 UIBezierPath 클래스를 사용하여 원을 그려봅시다.

Context를 이용하는 코드와 거의 유사합니다.

class PlayingCardView: UIView {
    override func draw(_ rect: CGRect) {
        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.red.setStroke()
        UIColor.green.setFill()
        path.stroke()
        path.fill()
    }
}

path를 생성하고 원 추가 및 속성들을 설정해줍니다.

마지막에 동일하게 stoke와 fill을 하도록 했습니다.

(context에서는 위와 같은 순서로 실행시 fill이 반영 안되었습니다.)

실행을 시켜보면 한번에 잘 적용된 것을 확인 할 수 있습니다.

context와 달리 stoke 후에도 경로가 남아있습니다.

 

Content Mode


위 코드로 앱을 실행 후 화면을 회전하게 될 경우 어떻게 나오는지 확인해봅시다.

위와 같이 화면 회전시 원이 찌그러집니다.

세로 모드일때는 원이 정상적으로 나오나, 가로 모드로 변경하게 될 경우 아래와 같이 타원형으로 변하게 됩니다.

왜냐하면 기본적으로 Bounds가 변경되었을 경우 비트를 해당 크기에 맞게 변경하기 때문입니다.( 기존 이미지를 재활용 )

이런 처리가 필요할때도 있겠지만 지금은 아닙니다.

 

우리가 원하는 것은 bounds가 변경되었을때 draw(rect)를 다시 부르는 것 입니다.

(그래서 새로운 공간에 다시 그리도록 합니다.)

 

그럼 수정해봅시다.

스토리보드에 들어가서 PlayingCardView를 선택후 attribute Inspector 메뉴에 들어갑니다.

가장 위에 Content Mode가 있습니다.

Scalce To Fill이 선택되어 있는는데 이 옵션이 바로 Bounds가 변경되었을때 거기에 맞춰 비트를 늘리거나 줄이도록 합니다.

여러 속성 중 Redraw를 선택해 줍니다.

Content Mode 옵션들

Redraw는 bounds가 변경되었시 다시 그려라는 옵션입니다.

이제 실행하여 화면을 회전해보면 정상적으로 원이 나타나는 것을 확인 할 수 있습니다.

 

둥근 모서리 처리


카드는 각진 사각형이 아닌 둥근 모서리를 갖기 때문에 거기에 맞춰 수정이 필요합니다.

View 내부에 둥근 모서리를 가지는 흰색 사각형을 그리도록 합니다.

override func draw(_ rect: CGRect) {
    let roundRect = UIBezierPath(roundedRect: bounds, cornerRadius: 16)
    roundRect.addClip()
    UIColor.white.setFill()
    roundRect.fill()
}

View의 크기에 꽉 차게 그리기 위해 사이즈는 bounds 변수를 이용합니다.

실행을 해보면 아래와 같이 나옵니다.

모서리가 둥글지 않습니다.

모서리가 여전히 각진 형태로 나오고 있습니다.

PlayingCardView의 배경색이 흰색이라 발생한 문제입니다.

배경색을 초록색으로 변경시킨 뒤 실행해봅니다.

흰색 네모의 모서리가 둥근것을 확인할 수 있습니다.

이번에는 초록색이 아닌 Clear Color로 변경합니다.

Opaque(불투명) 옵션도 체크 해제 합니다.

실행하면 아래와 같이 정상적으로 나타납니다.

 

 

모서리에 문양과 숫자 그리기


카드 좌측 상단과 우측하단에 문양과 숫자를 그려봅시다.

숫자를 그리고 그 아래에 문양이 있어야합니다.

라벨을 사용하여 추가할 것이며 필요한 속성으로는 가운데 정렬과 폰트 크기 입니다.

 

가운데 정렬 속성을 가진 문자열을 리턴하는 함수를 구현해 봅시다.

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
    return NSAttributedString(string: string, attributes: [.paragraphStyle:paragraphStyle, .font:font])
}

UIFontMetrics를 통하여 접근성 메뉴에서 폰트 크기 설정에 대해 대응합니다. 

맨 아래의 스크롤 뷰를 통하여 크기 조절이 가능합니다.

 

드로잉에 필요한 변수 추가하기


드로잉에 필요한 텍스트 정보를 가져오는 변수를 추가합니다.

var connerString: NSAttributedString {
    return centeredAttributedString(rank+"\n"+suit, fontSize: 0.0 )
}

 

문자열 생성시 필요한 데이터 변수를 추가합니다.

var rank: Int = 5
var suit: String = "❤️"
var isFaceUp: Bool = true

 

값이 변경될 경우 레이아웃을 갱신하도록 해봅시다.

didSet 키워드를 사용하면 됩니다.

var rank: Int = 5 { didSet { setNeedsDisplay(); setNeedsLayout() }}

setNeedsDisplay를 통해 draw(rect)를 재호출하도록 하고

setNeedDisplay를 통해 서브뷰들을 재배치하도록 합니다.

한번에 두 함수를 부르기 위해서 setNeedsDisplay() 뒤에 ;(세미콜론)을 사용했습니다.

 

 

Label 만들기


suit와 number를 출력한 label을 만들어봅시다.

좌측상단과 우측하단에 사용할 label 생성 함수를 추가합니다.

func createConnerLabel() -> UILabel {
    let label = UILabel()
    label.numberOfLines = 2 // 최대 라인수를 지정합니다.(0은 텍스트 라인에 맞춰 표시)
    addSubview(label) // 현재 뷰에 추가합니다.
    return label
}

numberOfLines 변수는 나타낼수 있는 최대 라인수입니다.(1인 경우 한줄만 표시되기 때문에 suit가 출력 안됩니다.)

0도 사용시 라인 수에 상관없이 모든 라인이 출력됩니다.

 

라벨을 위한 멤버변수도 추가합니다.

private var upperLeftConnerLabel = createConnerLabel()
private var lowerRightConnerLabel = createConnerLabel()

위과 같이 정의시 에러가 발생합니다.

createConnerLabel은 정적 함수가 아니기 때문에 발생한 에러입니다.

lazy 키워드를 추가합니다.

lazy private var upperLeftConnerLabel = createConnerLabel()
lazy private var lowerRightConnerLabel = createConnerLabel()

 

Label의 위치를 지정해 줍니다.

 위치를 정해주는 시점은 bounds가 변경될때마다 새로 지정해줘야합니다.

좌상단 라벨의 위치는 고정이지만 우하단은 bounds에 영향을 받기 때문에 새로 지정이 필요합니다.

bounds 바뀔때 마다 호출해줘야하는 코드는 어디에 넣어야 할까요?

layoutSubviews() 입니다.( setNeedsLayout 호출시에도 호출됩니다. )

setNeedDisplay 호출시 -> draw(rect)

setNeedsLayout 호출시 -> layoutSubviews

 

UIView의 frame은 위치와 크기를 지정할 수 있습니다.

override func layoutSubviews() {
   super.layoutSubviews()
        
   upperLeftConnerLabel.frame.origin = bounds.origin.offsetBy(dx: cornerOffset, dy: cornerOffset)
}

 

Label의 텍스트도 지정해 줍시다.

func configureCornerLabel(_ label: UILabel) {
    label.attributedText = connerString
    label.frame.size = .zero
    label.sizeToFit()
    label.isHidden = !isFaceUp
}

Label의 크기를 텍스트에 맞추기 위해서

 frame.size를 CGSize.zero로 설정하고 sizeToFit를 호출하도록 했습니다.

layoutSubviews 함수를 수정 후 실행해봅시다.

override func layoutSubviews() {
    super.layoutSubviews()
        
    configureCornerLabel(upperLeftConnerLabel)
    upperLeftConnerLabel.frame.origin = bounds.origin.offsetBy(dx: cornerOffset, dy: cornerOffset)
}

좌측상단에 정상적으로 표시되었습니다.

 

우측 하단에도 표시되도록 해봅시다.

override func layoutSubviews() {
    super.layoutSubviews()
        
    configureCornerLabel(upperLeftConnerLabel)
    upperLeftConnerLabel.frame.origin = bounds.origin.offsetBy(dx: cornerOffset, dy: cornerOffset)
        
    configureCornerLabel(lowerRightConnerLabel)
    lowerRightConnerLabel.frame.origin = bounds.origin.offsetBy(dx: bounds.maxX,dy: bounds.maxY)
    .offsetBy(dx: -lowerRightConnerLabel.frame.width, dy: -lowerRightConnerLabel.frame.height)
    .offsetBy(dx: -cornerOffset, dy: -cornerOffset)
}

좌표만 우측 하단에 나타나도록 하였습니다.

위치는 정상이나 기호와 숫자가 뒤집어서 출력되어야 합니다.

회전을 주도록 해봅시다.

 

모든 뷰에는 transform이라는 변수가 존재합니다.

transform은 affine 변환이라 불리며 크기 변경, 평행 이동, 회전을 줄 수 있습니다.

즉 UIView를 가져와 크기를 늘리거나 위치를 이동하거나 회전을 하고 싶을때 이 변수를 사용하면 됩니다. 

이때까지는 frame값을 변경했지만 traform을 통해서도 변환이 가능합니다.

그리고 비트 단위의 변환을 사용합니다.(확대시 이미지가 깨질 수 있음)

override func layoutSubviews() {
    super.layoutSubviews()
        
    configureCornerLabel(upperLeftConnerLabel)
    upperLeftConnerLabel.frame.origin = bounds.origin.offsetBy(dx: cornerOffset, dy: cornerOffset)
        
    configureCornerLabel(downRightConnerLabel)
       
    downRightConnerLabel.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi)
    downRightConnerLabel.frame.origin = bounds.origin.offsetBy(dx: bounds.maxX,dy: bounds.maxY)
        .offsetBy(dx: -downRightConnerLabel.frame.width, dy: -downRightConnerLabel.frame.height)
        .offsetBy(dx: -cornerOffset, dy: -cornerOffset)
}

CGAffineTransform.init(rotationAngle: CGFloat.pi)를 추가하여 180도 회전하도록 했습니다.

강의에서는 translatedBy도 함께 적용해주는데 영향이 없어 뺐습니다.....

frame.origin를 설정하는 코드로 인해 위치가 바로 잡히는것으로 보입니다.

아래는 강의에서 사용한 이동 후 회전하는 코드입니다.

downRightConnerLabel.transform = CGAffineTransform.identity
            .translatedBy(x: downRightConnerLabel.frame.width, y: downRightConnerLabel.frame.height)
            .rotated(by: CGFloat.pi)

우측 하단의 기호와 숫자가 정상적으로 오는것을 확인 할 수 있습니다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함