View / UIBezierPath / CAShapeLayer에 패턴 넣기 (feat. Accessibility)
안녕하세요 :) Zedd입니다.
HIG ) Color and Contrast 글에서 봤듯이,
color에만 의존하여 object를 구분하거나 중요한 정보를 전달하지 말 것.
앱에서 color를 사용하여 정보를 전달하는 경우, 색맹 사용자도 이해할 수 있도록 text labels 또는 glyph를 제공해야 한다.
하지만,
왼쪽 차트에 대해서는 생각을 해본적이 없는데, 당연히 위와같은 차트는
색약이나 색맹을 가지고 있는 사람들이 보게 되면 색상 구분이 되지않습니다.
오른쪽 사진처럼 각 파이에 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에 넣기
보통 이런 차트는 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에 패턴을 그리는 더 좋은 방법이 있다면...댓글 부탁드립니다. 🙇