ios 코어 그래픽 1

ios 코어 그래픽 1

 

 

 Core Graphics에서 도형을 그리려면 Core Graphics에 추적할 선(예: 더하기를 위한 두 개의 직선) 또는 채울 선(예: 원)을 알려주는 경로를 정의합니다.

 

 경로에 대해 알아야 할 세 가지 기본 사항이 있습니다.

 

 1.Path는 획을 그어 채울 수 있습니다.

 2.획은 현재 획 색상으로 패스의 윤곽을 그립니다.

 3.채우기(fill)는 현재 채우기 색상으로 닫힌 경로를 채웁니다.

 

 Core Graphics 경로를 만드는 쉬운 방법은 UIBezierPath라는 편리한 클래스입니다.

 이 클래스를 사용하면 사용자에게 친숙한 API로 경로를 개발할 수 있습니다. 경로는 선, 곡선, 직사각형 또는 일련의 연결된 점을 기반으로 할 수 있습니다.

 

+- 버튼 코드

 

import UIKit

//참고
//https://www.kodeco.com/8003281-core-graphics-tutorial-getting-started

@IBDesignable
class PushButton: UIButton {

    /// 배경색상
    @IBInspectable var fillColor:UIColor = .green
    
    /// + 버튼 유무
    @IBInspectable var isAddButton:Bool = true
    
    
    private struct Constants{
        static let plusLineWidth = 3.0
        static let plusButtonScale = 0.6
        static let halfPointShift = 0.5
    }
    
    private var halfWidth:CGFloat{
        return bounds.width / 2
    }

    private var halfHeight:CGFloat{
        return bounds.height / 2
    }
    
    // UIBezierPath를 사용하여 경로를 만든 다음 녹색으로 채웁니다. PushButton.swift를 열고 다음 메서드를 추가합니다.
    override func draw(_ rect: CGRect) {
        
        
        let path = UIBezierPath(ovalIn: rect)
        
        /// 배경색상
        fillColor.setFill()
        path.fill()
        
        
        // 너비 및 높이 변수 설정
        // 수평선의 경우
        let plusWidth = min(bounds.width, bounds.height) * Constants.plusButtonScale
        let halfPlusWidth = plusWidth / 2
        
        
        print("*halfWidth : \(halfWidth)")
        print("*halfHeight : \(halfHeight)")
        print("*plusWidth : \(plusWidth)")
        print("*halfPlusWidth : \(halfPlusWidth)")
        print("")
        
        let plusPath = UIBezierPath()
            
        plusPath.lineWidth = Constants.plusLineWidth
        
        
        // 경로의 시작점으로 이동
        // 가로 획의 시작 부분으로
        plusPath.move(to: CGPoint(x: halfWidth - halfPlusWidth, y: halfHeight + Constants.halfPointShift))
        
        print("*move x: \(halfWidth - halfPlusWidth) 부터")
        print("*move y: \(halfHeight) 부터")

        
        
        // 획의 끝에서 경로에 점을 추가합니다
        plusPath.addLine(to: CGPoint(x: halfWidth + halfPlusWidth, y: halfHeight + Constants.halfPointShift))
        
        print("*addLine x: \(halfWidth + halfPlusWidth) 까지")
        print("*addLine y: \(halfHeight) 까지")
        

        
        /*
         경로(Path)는 단순히 점으로 구성된다는 점을 기억하십시오.
         개념을 이해하는 쉬운 방법은 손에 펜이 있다고 상상하는 것입니다.
         페이지에 두 개의 점을 배치합니다.
         그런 다음 시작점에 펜을 놓고 다음 점까지 선을 그립니다.
         이것이 기본적으로 move(to:) 및 addLine(to:)을 사용하여 위의 코드로 수행한 작업입니다.
         */
        
        /// 더하기 버튼이면
        if isAddButton {
            plusPath.move(to: CGPoint(x: halfWidth, y: halfHeight - halfPlusWidth + Constants.halfPointShift))
            plusPath.addLine(to: CGPoint(x: halfWidth, y: halfHeight + halfPlusWidth + Constants.halfPointShift))
            
        }
        

        // 선 색상
        UIColor.white.setStroke()
        
        //그리기
        plusPath.stroke()
    }


}

 

핵심내용

 각 UIView에는 그래픽 컨텍스트(context)가 있습니다. 뷰에 대한 모든 드로잉은 기기의 하드웨어로 전송되기 전에 이 컨텍스트(context)로 렌더링됩니다.

 

 iOS는 뷰를 업데이트해야 할 때마다 draw(_:)를 호출하여 컨텍스트(context)를 업데이트합니다. 이것은 다음과 같은 경우에 발생합니다.

 

 1.view가 화면에 새롭게 나타날때.

 2.그 위에 있는 다른 view가 이동될때

 3.보기의 숨겨진 속성이 변경될때.

 

 view 에서 setNeedsDisplay() 또는 setNeedsDisplayInRect()를 명시적으로 호출합니다.

 

 참고: draw(_:)에서 수행된 모든 드로잉은 뷰의 그래픽 컨텍스트로 이동합니다.

 draw(_:) 외부에서 그리는 경우 고유한 그래픽 컨텍스트를 만들어야 합니다.

 이 튜토리얼에서는 아직 Core Graphics를 사용하지 않았습니다.

 왜냐하면 UIKit에는 많은 Core Graphics 기능에 대한 래퍼가 있기 때문입니다.

 예를 들어 UIBezierPath는 하위 수준 Core Graphics API인 CGMutablePath의 래퍼입니다.

 

 

픽셀 관련해서

 최초의 iPhone 시절에는 포인트와 픽셀이 같은 공간을 차지하고 같은 크기였습니다.

 이것은 그것들을 기본적으로 동일하게 만들었습니다.

 레티나 아이폰이 나왔을 때 그들은 같은 수의 포인트에 대해 화면에 4배의 픽셀을 자랑했습니다.

 마찬가지로 iPhone 8 Plus는 동일한 지점의 픽셀 수를 다시 늘렸습니다.

 

 아래는 점이 회색과 흰색으로 표시된 12×12 픽셀의 그리드입니다.

 iPad 2는 포인트를 픽셀로 직접 매핑하므로 1x입니다.

 iPhone 8은 한 지점에 4개의 픽셀이 있는 2x 레티나 화면입니다.

 마지막으로, iPhone 8 Plus는 한 지점에 9픽셀이 있는 3x 레티나 화면입니다.

 

 

부채꼴 호

 

개념

 

 

빨간색 화살표는 호가 시작되고 끝나는 위치를 보여주며 시계 방향으로 그립니다.

 

 3π/4 라디안(135º에 해당) 위치에서 시계 방향으로 π/4 라디안(45º)에서 호를 그립니다.

 

 라디안은 일반적으로 도 대신 프로그래밍에 사용되며 라디안으로 생각하면 원으로 작업할 때마다 도로 변환할 필요가 없습니다. 주의: 나중에 호 길이를 파악해야 하며 라디안이 적용됩니다.

 

 반지름이 1.0인 단위원에서 호의 길이는 라디안 단위의 각도 측정값과 같습니다.

 

 예를 들어 위의 다이어그램을 보면 0º에서 90º까지의 호 길이는 π/2입니다.

 

 실제 상황에서 호의 길이를 계산하려면 단위 원호 길이에 실제 반지름을 곱하십시오.

 

 위의 빨간색 화살표의 길이를 계산하려면 2π ​​– 화살표 끝(3π/4) + 화살표 지점(π/4) = 3π/2와 같이 라디안 수를 계산해야 합니다.

 

 각도는 360º – 135º + 45º = 270º입니다.

 

코드


/*
 여기에서 상수가 있는 구조체를 만들었습니다. 그림을 그릴 때 사용합니다. 홀수인 numberOfGlasses는 하루에 마시는 목표 잔 수입니다. 이 수치에 도달하면 카운터가 최대가 됩니다.

 스토리보드에서 업데이트할 수 있는 세 개의 @IBInspectable 속성도 생성했습니다.
 카운터는 소비된 안경의 수를 추적합니다.
 이는 특히 counter view를 테스트하기 위해 스토리보드에서 변경할 수 있는 기능이 있는 것이 유용하기 때문에 @IBDesignable 속성입니다.

 */
import UIKit


//참고
//https://www.kodeco.com/8003281-core-graphics-tutorial-getting-started
@IBDesignable class CounterView: UIView {

    //MARK: 상수
    private struct Constants{
        
        static let numberOfGlasses = 8
        
        static let lineWidth:CGFloat = 5.0

        static let arcWidth:CGFloat = 76
        
        ///2.5
        static var halfOfLineWidth:CGFloat{
            return lineWidth / 2
        }
    }
    
    
    
    @IBInspectable var counter:Int = 5{
        didSet{
            if counter <= Constants.numberOfGlasses {
                setNeedsDisplay()
            }
        }
    }
    
    
    @IBInspectable var outlineColor:UIColor = UIColor.blue
    @IBInspectable var counterColor:UIColor = UIColor.orange
    
    
    //뷰 가로세로 크기 : 230, 230
    override func draw(_ rect: CGRect) {
        
        
        print("")
        print("====== 바닥에 호 기본값 =====")
        print("가로 :\(rect.width)")
        print("세로 :\(rect.height)")
        print("잔 개수 :\(Constants.numberOfGlasses)")
        print("선 두께 :\(Constants.lineWidth)")
        print("호 두께 :\(Constants.arcWidth)")
        print("라인가로의반 :\(Constants.halfOfLineWidth)")
        print("===========\n")

        
        /// test 용
        //self.printOneCmCircleInfo()
        
        
        //1. 호를 회전할 중심점을 정의합니다.
        let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
        
        //2. 뷰의 최대 치수를 기준으로 반지름을 계산합니다.
        let radius = max(bounds.width, bounds.height)
        
        //3.호의 시작 각도와 끝 각도를 정의합니다. (135 도)
        let startAngle:CGFloat = 135 * .pi / 180
        
        //(45 도)
        let endAngle:CGFloat = 45 * .pi / 180
        
        
        print("*center (중심): \(center)")
        print("*(지름): \(radius)")
        print("*startAngle (135도 시작 라디안): \(startAngle)")
        print("*endAngle (45도 끝 라디안): \(endAngle)")
        print("*실제 반지름 : \(radius/2 - Constants.arcWidth/2)")
        
        /*
         *center (중심): (115.0, 115.0)
         *(지름): 230.0
         *startAngle (시작 라디안): 2.356194490192345
         *endAngle (끝 라디안): 0.7853981633974483
         *반지름 : 77.0
         *halfWidth : 25.0
         *halfHeight : 25.0
         *plusWidth : 30.0
         *halfPlusWidth : 15.0
         */
        
        //MARK: 부채꼴 호 그리기
        //4. 정의한 중심점, 반지름 및 각도를 기준으로 경로를 만듭니다.
        let path = UIBezierPath(
            arcCenter: center,
            radius: radius / 2 - Constants.arcWidth / 2 ,                //반지름
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: true)
        
        // 경로를 마지막으로 칠하기 전에 선 너비와 색상을 설정합니다.
        //호 두께 76 , darkgray
        path.lineWidth = Constants.arcWidth
        UIColor.darkGray.setStroke()
        path.stroke()
        
        
        
        //[아웃라인 호 그리기]
        // 360도 - 135도 = 225도
        // 225도 + 45도 = 270도
        // 호의 전체 각도 270도 = 4.71238898038469 라디안
        
        //1.outlineEndAngle은 호가 끝나는 각도입니다. 현재 카운터 값을 사용하여 계산됩니다.
        //1 - 먼저 두 각도의 차이를 계산합니다.
        //호의 전체 각도
        let angleDifference:CGFloat = (2 * .pi - startAngle) + endAngle
        
        print("*2 * .pi - start 라디안: \(2 * Double.pi - startAngle)")
        print("*end 라디안 : \(endAngle)")
        print("*angleDifference(호의 전체 각도) : \(angleDifference)")
        /*
         *2 * .pi - start 라디안  : 3.9269908169872414
         *end 라디안              : 0.7853981633974483
         *angleDifference(호의 전체 각도)  : 4.71238898038469
         */
        
        
        //2 - 그런 다음 각 단일 유리에 대한 arc를 계산합니다.
        let arcLengthPerGlass = angleDifference / CGFloat(Constants.numberOfGlasses)
        
        
        print("*arcLengthPerGlass(각 잔의 라디안) : \(arcLengthPerGlass)")
        //*angleDifference(각 잔의 라디안) : 0.5890486225480862
        //*33.75도 = 0.589048622548086 라디안
        
        
        
        //3 - 그런 다음 취한 실제 잔으로 곱합니다.
        let outlineEndAngle = arcLengthPerGlass * CGFloat(counter) + startAngle
        print("*outlineEndAngle(실제 마신 잔의 마지막 각도) : \(outlineEndAngle)")
        //*outlineEndAngle(실제 마신 잔의 마지막 라디안) : 5잔일 때 = 5.301437602932776
        //식 : 0.589048622548086 라디안(33.75도)  * 5(잔) + 2.356194490192345(135도 시작 라디안)

        
        
        //2.outlinePath는 외부(라인) 호입니다.
        //UIBezierPath()는 이 호가 단위원이 아니므로 반경을 사용하여 호의 길이를 계산합니다.
        let outerArcRadius = bounds.width/2 - Constants.halfOfLineWidth  // 115 - 2.5
        print("*outerArcRadius : \(outerArcRadius)")
        //*outerArcRadius : 112.5

        
        //MARK: 바깥 호 라인 그리기
        //[!!] 호 그리기 패스
        let outlinePath = UIBezierPath(arcCenter: center,
                                       radius: outerArcRadius,
                                       startAngle: startAngle,
                                       endAngle: outlineEndAngle,
                                       clockwise: true)
        
        // 3.첫 번째 호에 내부(라인) 호를 추가합니다.
        //각도는 같지만 반대로 그립니다. 이것이 시계 방향이 false로 설정된 이유입니다. 또한 내부 호와 외부 호 사이에 자동으로 선을 그립니다.
        let innerArcRadius = bounds.width/2 - Constants.arcWidth + Constants.halfOfLineWidth    // (115 - 76 - 2.5)
        print("*innerArcRadius : \(innerArcRadius)")
        //*innerArcRadius : 41.5
        
        outlinePath.addArc(withCenter: center,
                           radius: innerArcRadius,
                           startAngle: outlineEndAngle,
                           endAngle: startAngle,
                           clockwise: false)
        
   
        outlinePath.close()
        UIColor.green.setStroke()
        outlinePath.lineWidth = Constants.lineWidth
        outlinePath.stroke()
        
        
        
        //MARK: 마커그리기
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        
        //1.원래의 상태 저장
        context.saveGState()
        UIColor.red.setFill()
        
        let markerWidth:CGFloat = 5.0
        let markerSize:CGFloat = 10.0
        let markerRect = CGRect(x: -markerWidth / 2,
                                y: 0.0,
                                width: markerWidth,
                                height: markerSize)
        
        //마커 패스
        let markerPath = UIBezierPath(rect: markerRect)
        
        //컨텍스트의 원래 왼쪽탑의 위치를 중심 주위에서 회전이 발생하도록 컨텍스트 중심으로 이동(옮김)
        context.translateBy(x: rect.width / 2, y: rect.height / 2)
        
        
        context.saveGState()

        context.rotate(by: 78 * Double.pi / 180)

        
        markerPath.fill()
        
        context.restoreGState()
        
        
        
        var textEndAngle = 0.0
        
        for i in 1...Constants.numberOfGlasses {
            
            //왼쪽탑의 위치가 center에 위치하고 있는 컨텍스트 저장 (center 컨텍스트 저장)
            context.saveGState()
            
            // 식 : (각 잔의 라디안 0.5890486225480862) * (index) + 시작 라디안((135도 시작 라디안): 2.356194490192345) - .pi / 2 (1.5707)
            let angle = arcLengthPerGlass * CGFloat(i) + startAngle - .pi / 2

            /*
             참고 1. radian을 도(°)로 변환하는 법
             라디안에 180을 곱하고 파이(3.141592...)를 나누면 됩니다.
             */
            print("\(i) 번째 마커의 라디안 : \(angle)")
            print("\(i) 번째 마커의 각도 : \(angle * 180 / .pi)")
            
            /*
             1 번째 마커의 라디안 : 1.3744467859455343
             1 번째 마커의 각도 : 78.74999999999999
             2 번째 마커의 라디안 : 1.9634954084936207
             2 번째 마커의 각도 : 112.5
             3 번째 마커의 라디안 : 2.552544031041707
             3 번째 마커의 각도 : 146.25000000000003
             4 번째 마커의 라디안 : 3.141592653589793
             4 번째 마커의 각도 : 180.0
             5 번째 마커의 라디안 : 3.730641276137879
             5 번째 마커의 각도 : 213.74999999999997
             6 번째 마커의 라디안 : 4.319689898685965
             6 번째 마커의 각도 : 247.49999999999997
             7 번째 마커의 라디안 : 4.908738521234052
             7 번째 마커의 각도 : 281.25
             8 번째 마커의 라디안 : 5.497787143782138
             8 번째 마커의 각도 : 315.0
             */
            

            // 돌리고
            context.rotate(by: angle)
            
            // 옮기고(이동시키고)
            context.translateBy(x: 0, y: rect.height / 2 - markerSize)

            // 칠하고
            markerPath.fill()
            

            // 7 - 다음 회전을 위해 중심 컨텍스트 (왼쪽탑의 위치가 컨텍스트의 가운데 있는 상태)를 복원합니다.
            context.restoreGState()

            
            
            let textXPoint = center.x + (radius / 2 ) * cos(startAngle + textEndAngle)
            let textYPoint = center.y + (radius / 2 ) * sin(startAngle + textEndAngle)

            let textLayer = CATextLayer()
            textLayer.frame = CGRect(origin: CGPoint(x: textXPoint - 5, y: textYPoint - 5), size: CGSize(width: 15.0, height: 15.0))
            textLayer.string = "\(i)"
            textLayer.fontSize = 13.0
            self.layer.addSublayer(textLayer)


            textEndAngle += (33.75 * .pi / 180)
            

        }
        
        //CATextLayer 흐림 검색
        
        
        //회전과 변환 전에 컨텍스트의 원래 상태(마커 패스 그리기전 상태)를 복원합니다.
        context.restoreGState()
        
        
//
//        var textEndAngle = 0.0
//
//        context.translateBy(x: 0.0, y: CGRectGetHeight(rect))
//        context.scaleBy(x: 1.0, y: -1.0)
//
//        for i in 1...Constants.numberOfGlasses {
//
//            let textXPoint = center.x + (radius / 2 ) * cos(startAngle + textEndAngle  )
//            let textYPoint = center.y + (radius / 2 ) * sin(startAngle + textEndAngle  )
//
//            print("textXPoint : \(textXPoint) , textYPoint:\(textYPoint)")
//
//            let aFont = UIFont(name: "Damascus", size: 8)
//            let attr:CFDictionary = [NSAttributedString.Key.font:aFont!,
//                                     NSAttributedString.Key.foregroundColor:UIColor.white] as CFDictionary
//            /// 숫자 텍스트
//            guard let text = CFAttributedStringCreate(nil, "\(i)" as CFString, attr) else { return }
//            let line = CTLineCreateWithAttributedString(text)
//            let bounds = CTLineGetBoundsWithOptions(line, .useOpticalBounds)
//
//            context.setLineWidth(1.0)
//            context.setTextDrawingMode(.fillStroke)
//
//            let xn = textXPoint - bounds.width / 2
//            let yn = textYPoint - bounds.midY
//
//            context.textPosition = CGPoint(x: xn, y: yn)
//
//            CTLineDraw(line, context)
//
//            textEndAngle += (33.75 * .pi / 180)
//
//        }
//        context.restoreGState()

        
        
    }
    
    /*
     4.경로를 닫으면 자동으로 호의 다른 쪽 끝에 선이 그려집니다.
     CounterView.swift의 카운터를 5로 설정하면 스토리보드에서 CounterView가 다음과 같이 표시됩니다.
     */
    
    
    //반지름이 1.0 인 원의 호 길이 정보
    func printOneCmCircleInfo() {

        let circlePi = Float(Double.pi * 2)
        
        let end = Float((Double.pi * 3) / 4)
        
        let start = Float(Double.pi / 4)
        
        let zeroToNinety = Float(Double.pi / 2)

        
        print("*반지름이 1.0 cm 인 원의 호 길이")
        print("*circlePi : \(circlePi) cm")
        print("*end : \(end) cm")
        print("*start : \(start) cm")
        print("*길이 : \(circlePi - (end + start)) cm")
        print("*0도 에서 90도 까지 거리: \(zeroToNinety) cm")
        print("*원의 전체 길이 : \(zeroToNinety * 4) cm")
        print("")
        
        /*
         *반지름이 1.0 cm 인 원의 호 길이
         *circlePi : 6.2831855 cm
         *end : 2.3561945 cm
         *start : 0.7853982 cm
         *길이 : 3.1415927 cm
         *0도 에서 90도 까지 거리: 1.5707964 cm
         *원의 전체 길이 : 6.2831855 cm
         */
    }
}


/*
 빨간색 화살표는 호가 시작되고 끝나는 위치를 보여주며 시계 방향으로 그립니다.
 
 3π/4 라디안(135º에 해당) 위치에서 시계 방향으로 π/4 라디안(45º)에서 호를 그립니다.

 라디안은 일반적으로 도 대신 프로그래밍에 사용되며 라디안으로 생각하면 원으로 작업할 때마다 도로 변환할 필요가 없습니다. 주의: 나중에 호 길이를 파악해야 하며 라디안이 적용됩니다.

 반지름이 1.0인 단위원에서 호의 길이는 라디안 단위의 각도 측정값과 같습니다.
 
 예를 들어 위의 다이어그램을 보면 0º에서 90º까지의 호 길이는 π/2입니다.
 
 실제 상황에서 호의 길이를 계산하려면 단위 원호 길이에 실제 반지름을 곱하십시오.

 위의 빨간색 화살표의 길이를 계산하려면 2π ​​– 화살표 끝(3π/4) + 화살표 지점(π/4) = 3π/2와 같이 라디안 수를 계산해야 합니다.

 각도는 360º – 135º + 45º = 270º입니다.
 
 즉석 수학 수업이 끝났습니다!
 */

 

 

viewController


import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var counterView: CounterView!
    
    @IBOutlet weak var counterLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    
    @IBAction func pushButtonPressed(_ sender: PushButton) {
        if sender.isAddButton {
            
            if counterView.counter <= 7{
                counterView.counter += 1
            }
            
        }else{
            if counterView.counter > 0{
                counterView.counter -= 1
            }
        }
        
        counterLabel.text = String(counterView.counter)
        
    }
    
}

 

 

CoreGraphicstutor.zip
0.06MB

 

 

참고

https://www.kodeco.com/8003281-core-graphics-tutorial-getting-started