iOS

View / UIBezierPath / CAShapeLayer에 패턴 넣기 (feat. Accessibility)

Zedd0202 2021. 5. 29. 15:38
반응형

 

안녕하세요 :) Zedd입니다.

HIG ) Color and Contrast 글에서 봤듯이,

color에만 의존하여 object를 구분하거나 중요한 정보를 전달하지 말 것. 
앱에서 color를 사용하여 정보를 전달하는 경우, 색맹 사용자도 이해할 수 있도록 text labels 또는 glyph를 제공해야 한다.

하지만,

https://ko.wikipedia.org/wiki/파일:United_States_Balance_of_Trade_Deficit-pie_chart.svg

왼쪽 차트에 대해서는 생각을 해본적이 없는데, 당연히 위와같은 차트는 

색약이나 색맹을 가지고 있는 사람들이 보게 되면 색상 구분이 되지않습니다.

오른쪽 사진처럼 각 파이에 Label을 달아주는것도 좋은 방법이지만,

그럴 수 없을때는 각 파이에 "패턴"을 넣어주는게 좋다고 합니다.

color에만 의존하여 object를 구분하거나 중요한 정보를 전달하지 말 것. 

이 논리를 왜 차트에는 적용을 못했을까! 싶어요. 패턴 이야기를 들었을때는 머리가 띵했습니다. 

아직 공부해야 할 게 많구나..

아무튼 View에 패턴을 넣어보겠습니다.

 

# View에 넣기

1. UIColor(patternImage:) 사용

self.view.backgroundColor = UIColor(patternImage: UIImage(named: "stripe_green")!)

가장 간단한 방법입니다.

하지만,

하지만 우리가 준 이미지는 주어진 영역을 덮기 위해 필요에 따라 타일링되기 때문에..

저렇게 어긋나는 어색한 부분이 있을 수 있습니다.

 

2. CIFilter사용.

CIFilter에 감사하게 도..CIStripesGenerator라는 것이 있습니다.

1번처럼 Color를 넣는게 아니라 Image를 만들어서 넣는것이기 때문에..UIImageView가 필요합니다. 

CIFilter를 사용하는 방법은 iOS ) CIFilter 사용해보기 글을 참고해주세요. 

간단한 Flow는 다음과 같습니다.

1. Filter만들기

2. Filter에서 outputImage(CIImage타입)가져오기

3. CIImage -> CGImage -> UIImage로 만들기

4. UIImageView에 적용.

 

1. Filter만들기

var stripeFilter: CIFilter? {
    return CIFilter(name: "CIStripesGenerator", parameters: [
        "inputColor0" : CIColor(color: self.stripeColor),
        "inputColor1" : CIColor(color: self.backgroundColor),
        "inputWidth" : self.stripeWidth,
        "inputSharpness": self.sharpness
    ])
}

2. Filter에서 outputImage(CIImage타입)가져오기

guard let stripedCIImage = self.stripeFilter?.outputImage else { return nil }

3. CIImage -> CGImage -> UIImage로 만들기

func createCGImage(ciImage: CIImage) -> CGImage? {
    let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
    return self.context.createCGImage(ciImage, from: rect)
}

private func createUIImage(from ciImage: CIImage) -> UIImage? {
    guard let cgImage = self.createCGImage(ciImage: ciImage) else { return nil }
    return UIImage(cgImage: cgImage)
}

4. UIImageView에 적용.

3번에서 만든 UIImage를 적용하기 

 

사실 코드 나열한것만 보면 하나도 이해 안가실텐데..

var stripeFilter: CIFilter? {
    return CIFilter(name: "CIStripesGenerator", parameters: [
        "inputColor0" : CIColor(color: self.stripeColor),
        "inputColor1" : CIColor(color: self.backgroundColor),
        "inputWidth" : self.stripeWidth,
        "inputSharpness": self.sharpness
    ])
}

이것만 보면 됩니다. 

inputColor0 -> 스트라이프 색상

inputColor1 -> 배경 색상

inputWidth -> 스트라이프 Width

inputSharpness -> 선명도 값 (이건 이따가 그림으로..기본값은 1.0)

let size = self.myImageView.frame.size
let generator = StripesGenerator(size: size,
                                 backgroundColor: .white,
                                 stripeColor: .blue,
                                 stripeWidth: 2)
self.myImageView.image = generator.apply()

background를 하얀색으로, stripe색상을 파란색, width는 2로 줬습니다. Sharpness는 안줬어요.

그럼 이렇게 나오게 됩니다. 아 눈아파

배경을 보면 초록색 스트라이프가 파란색 스트라이프처럼 딱 90도가 아니라 기울어져 있는게 보이시죠? 

스트라이프에 rotation을 줘보겠습니다.

let size = self.myImageView.frame.size
let generator = StripesGenerator(size: size,
                                 backgroundColor: .white,
                                 stripeColor: .blue,
                                 stripeWidth: 10, 
                                 angle: .pi / 4) ✅
self.myImageView.image = generator.apply()

생성자에 angle을 넣습니다. pi / 4. 즉 180 / 4 == 45입니다.

(width는 눈이 아파서 10으로 늘려줬음..)

StripesGenerator안에서는 angle이 zero가 아니면 rotationFilter를 적용하도록 되어있습니다. 

func rotationFilter(inputCIImage: CIImage) -> CIFilter? {
    let inputTransform = NSValue(cgAffineTransform: CGAffineTransform(rotationAngle: self.angle))
    return CIFilter(name: "CIAffineTransform", parameters: [
        "inputImage" : inputCIImage,
        "inputTransform" : inputTransform
    ])
}

이렇게요.

그리고 똑같이

3. CIImage -> CGImage -> UIImage로 만들기

4. UIImageView에 적용.

과정을 거치면 됩니다.

눈이 편-안하네요. 이렇게 만들어지게 됩니다.

StripesGenerator의 전체코드는 github > StripesGenerator.swift을 참고해주세요. 

전체 코드도 github에 있습니다. 

 

🙋 : Sharpness한대매 

🧑‍💻 : Sharpness는 선명도 값이라고 보면 될 것 같아요. 0.2같은 값을 주게 되면 

이렇게 뭔가 흐-릿..한 느낌을 주게 됩니다. 

 

# UIBezierPath 또는 CAShapeLayer에 넣기 

 

https://ko.wikipedia.org/wiki/파일:United_States_Balance_of_Trade_Deficit-pie_chart.svg

보통 이런 차트는 UIBezierPath나 CAShapeLayer로 그리는데요.

이런것들에 패턴을 넣으려면 어떻게 해야할지!!

그래서 생각한게 위에서 한 것 처럼 UIBezierPath나 CAShapeLayer + CIFilter조합을 하면 되지 않을까 했는데..

관련 문서를 보면 

iOS에서는 안되고 ㅠㅠ...쉬운 방법을 찾다가 그냥 이미지를 조합하는 방식으로 생각해보았습니다.

위에서 언급한 UIColor(patternImage:)를 이용하는 것 인데요.

이런 이미지들을 assets에 넣어놓습니다.

 

[UIBezierPath]

제가 예전에 UIBezierPath (4) - Pie chart 글에서 

UIBezierPath를 이용하여 오른쪽 원형 차트를 그려봤는데요. 

저 colors.randomeElement()?.set()부분을 UIColor(patternImage:)로 만든 color를 사용하도록 하는 것입니다.

let images = [UIImage(named: "dotted")!,
              UIImage(named: "stripe_green")!,
              UIImage(named: "stripe_blue")!,
              UIImage(named: "stripe_pink")!]
let image: UIImage = images.randomElement()!

UIColor(patternImage: image).set()

뭐 이런식으로..!? (일단은 어떻게 하는지만 보기위해 강제언래핑으로,,)

그럼 뭐 이런식으로 만들어집니다.

한가지 유의해야할 점은 스트라이프 배경이 흰색이다 보니 배경도 흰색이면 가운데 처럼 원이 짤리는(?)듯한 착각을 주는데,

가장 오른쪽 그림처럼 border를 그려주는 BezierPath를 하나 그려주면 좋을듯,,,??

 

[CAShapeLayer]

UIBezierPath (5) - CAShapeLayer 글에서

이런 Pie 차트를 그려봤는데, (사실 애니메이션 안할거면 UIBezierPath로도 충분)

애니메이션과 같이 패턴을 줘야하는 상황이라면

sliceLayer.fillColor = nil
sliceLayer.strokeColor = color.randomElement().cgColor

이런 코드를 똑같이 UIColor(patternImage:)를 사용하도록 변경하면 됩니다.

let images = [UIImage(named: "dotted")!,
              UIImage(named: "stripe_green")!,
              UIImage(named: "stripe_blue")!,
              UIImage(named: "stripe_pink")!]
let image: UIImage = images.randomElement()!
sliceLayer.fillColor = nil
sliceLayer.strokeColor = UIColor(patternImage: image).cgColor ✅

이렇게요!

그러면

이런식으로 되는!? 

결과물은 나쁘지 않은데,

위에서 봤던 CIFilter처럼 제가 직접 값을 주고 stripe를 만들어낼 수 없다는 점 == 패턴마다 이미지가 필요하다.

요게 단점인것 같습니다..!

UIBezierPath나 CAShapeLayer에 패턴을 그리는 더 좋은 방법이 있다면...댓글 부탁드립니다. 🙇

반응형