본문 바로가기

학습 노트/iOS (2021)

036 ~ 042. Image and Color

Image View


이미지는 종횡비가 중요하기 때문에 원본의 것을 유지하도록 Aspect Fit이나 Aspect Fill의 사용 빈도가 높다.


결과


Aspect Fill을 사용하면 오른쪽 사진처럼 Image View의 영역(프레임) 밖으로 나가는 부분이 생기는데

Drawing의 Clips to Bounds를 해제하면
프레임 밖의 영역에서도 이미지를 보이도록 허용할 수 있다.
다만 ImagaeView의 크기와 사용하려는 이미지의 크기는 맞춰 주는 것이 좋다.
이미지를 확대하거나 축소하는 프로세스가 빠지기 때문에 성능에도 긍정적인 영향을 미친다.

 

Highlighted 속성에 별도의 이미지를 할당하면,
Highlighted 상태가 될 경우 해당 이미지를 표시하게 된다.
조건에 따라 이미지를 변경해야 할 때 사용하는 간단한 방법이다.

ImageView에 위와 같이 수직, 수평 제약을 추가하면
오른쪽의 사진과 같이 화면의 중앙에 배치되면서 이미지의 기본 크기로 바뀐다.
ImageView는 이미지를 가지고 있는 경우 Intrinsic Content Size를 가지게 된다.

따라서 지정했던 이미지를 모두 없애면 오류가 발생한다.
ImageView는 자신이 가지고 있는 이미지의 크기를 통해 자신의 크기를 결정하는데,
판단할 이미지가 없으면 자신의 크기를 정할 수 없다.

표시하는 모든 이미지의 크기가 동일하고, 모두 적합한 크기를 지니고 있다면 크기를 지정하지 않아도 되지만,
잠깐이라도 이미지를 가지고 있지 않은 상태가 될 수 있다면 오류가 발생하므로 크기는 직접 지정하는 것이 좋다.

이렇게 사이즈 제약을 추가하면 이미지가 사라져도 제약 오류는 발생하지 않게 된다.

 

Image Sequence

ImageView는 jpg나 png를 지원하지만 gif는 지원하지 않는다.
따라서 애니메이션 효과를 구현하려면 Image Sequence를 사용해 구현해야 한다.
단, 이때 사용하는 이미지는 사이즈와 스케일 펙터가 모두 동일해야 한다.

//
//  ImageAnimationViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/17.
//

import UIKit

class ImageAnimationViewController: UIViewController {
    let image = [
        UIImage(systemName: "speaker")!,
        UIImage(systemName: "speaker.1")!,
        UIImage(systemName: "speaker.2")!,
        UIImage(systemName: "speaker.3")!
    ]
    @IBOutlet weak var imageView: UIImageView!
    
    @IBAction func startAction(_ sender: Any) {
    }
    @IBAction func stopAction(_ sender: Any) {
    }
    
    

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

화면 구성과 코드 연결은 위와 같다.
코드에는 배열로 시스템 이미지 중 speaker 이미지 4개를 저장했다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	imageView.animationImages = image
}

뷰가 로드되면 imageView의 animationImages 속성에 앞서 생성한 배열을 전달한다.
이것으로 ImageSequence는 완료됐지만 자동으로 재생되지는 않는다.

@IBAction func startAction(_ sender: Any) {
	imageView.startAnimating()
}
@IBAction func stopAction(_ sender: Any) {
	imageView.stopAnimating()
}

그래서 위와 같이 startAnimating 메서드로 시작하고,
stopAnimating 메소드로 정지시켜야 한다.


결과


ImageSequence는 따로 시간을 지정하지 않으면 30 fps로 재생되고,
반복 횟수를 지정하지 않으면 종료할 때까지 무한히 반복한다.
또한, 정지했을 경우 ImageSequence는 표시되지 않기 때문에 정지 상태에서 표시할 다른 이미지를 함께 설정한다.

이렇게 ImageSequence가 실행 중이지 않을 때에는 설정한 이미지가 표시되게 된다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	imageView.animationImages = image
	
	imageView.animationDuration = 1.0
	imageView.animationRepeatCount = 2
}

ImageSequence의 재생 시간과 재생 횟수는 위와 같이 animationDuration와 animationRepeatCount로 지정한다.
기본값은 둘 다 0으로 Duration은 초 단위를 의미한다.


결과

 


애니메이션 길이는 1초로 두 번 재생되게 된다.

 

Image Basic


iOS에서는 PNG와 JPEG 이미지를 사용할 수 있지만 애플은 PNG를 권장하고 있다.

프로젝트에 이미지 추가하기

현행 iOS 프로젝트는 Asset 라이브러리를 사용해 프로젝트에 사용하는 이미지를 관리한다.
해당 Assets 라이브러리 폴더에 사용할 이미지를 끌어다 놓는 것으로 프로젝트에 이미지를 추가할 수 있다.

Asset 라이브러리의 구조는 위와 같다.

Image Set의 1x, 2x, 3x는 이미지의 Scale(스케일)로,
1x는 비 레티나 디바이스, 2x는 레티나 디바이스, 3x는 +나 맥스 기기들에 해당한다.

Image Well에 표시된 이미지를 원하는 Image Well로 끌어다 놓음으로써 스케일을 조정할 수 있다.

만약 이미지를 추가함과 동시에 스케일을 지정하고 싶다면 파일 이름 뒤에 '@2x', '@3x'의 접미어를 붙여 scale을 지정할 수 있다.
또한, 이렇게 추가한 이미지의 이름에는 접미어가 표시되지 않는다.
이미지의 이름은 프로젝트 내에서 이미지를 대표하게 되기 때문에 이름은 간결하고 명확하게 짓는 것이 좋다.

Image Set에서 우클릭을 하면 여러 기기명들을 확인할 수 있는데,
선택하면 위와 같이 해당 기기에 대한 부분이 추가된다.
이렇게 분리된 Image Set은 해당 기기에서만 사용되게 되며, 이외의 기기에선 Univeral의 Image Set이 사용되게 된다.

기기를 전부 선택에서 Image Set을 왕창 만들게 되어도 용량 문제나 기타 문제를 일으키지 않는다.
사용자에게 제공되는 설치 파일은 앱 구동에 필요한 최소한의 파일들로 구성되며,
해당 과정은 앱스토어에 등록하는 과정에서 자동으로 이루어진다.

 

ImageView에 이미지 추가하기

ImageView의 attribute inspector에서 image와 highlighted 속성은 표시할 이미지를 선택하는 속성이다.
해당 속성의 화살표를 누르면 프로젝트에서 기본적으로 제공하는 이미지와 직접 추가한 이미지들을 볼 수 있다.
이렇게 선택한 이미지는 ImageView에 표시된다.

이러한 과정은 LIbrary(라이브러리)에서도 가능한데,
라이브러리에서 Image Library 탭을 선택하고 원하는 이미지를 끌어다 놓는 것으로 충분하다.
단, 해당 ImageView에 이미 이미지가 존재하는 경우엔 새 ImageView를 생성하게 된다.

 

코드로 ImageView에 이미지 추가하기

//
//  ImageBasicViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/17.
//

import UIKit

class ImageBasicViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

위에서 구성한 화면은 코드와 위와 같이 연결된다.

코드에서 이미지를 다룰 때는 UIImage 클래스를 사용한다.
또한 이 UIImage에 assets에 추가한 이미지를 추가하는 경우 UIImage(named:) 생성자를 사용해 추가한다.

UIImage가 아닌 Image Literal을 사용하게 되면 코드에 작은 이미지가 생기는데

이것을 더블클릭하면 팝업으로 이미지 라이브러리가 표시되고, 선택하면 코드에 이미지가 표시된다.
시각적으로 정확하기 때문에 편하지만 Xcode 버전에 따라 제대로 표시되지 않거나, 지원하지 않는 경우도 있다.
또한, 후에 assets에서 이미지를 삭제하는 경우 충돌을 유발할 수 있다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	let image = UIImage(named: "cherry")
	let image2 = #imageLiteral(resourceName: "cherry")
	
	imageView.image = image
}

ImageView에 이미지를 표시하기 위해선 image 속성에 이미지를 전달해야 한다.

해당 속성은 Optional 형식으로, UIImage가 이미지를 제대로 받아오지 못할 경우 반환하는 nil을 소화할 수 있다.
또한, nil이 전달되는 경우 단순히 이미지를 표시하지 않을 뿐이다.


결과

 


if let size = image?.size {
	print(size)
}
결과

(350.0, 350.0)

이미지의 사이즈를 원하는 경우 위와 같이 size 속성에 접근해 구할 수 있다.
단, 해당 사이즈는 px단위가 아닌 pt단위로, 픽셀 단위의 수피를 원하면 scale을 곱해줘야 한다.

if let size = image?.size, let scale = image?.scale {
	let realSize = CGSize(width: size.width * scale, height: size.height * scale)
	print(realSize)
}
결과

(700.0, 700.0)

UIIamge 인스턴스와 Image Literal은 불변 객체이기 때문에 비트맵 데이터에 직접 접근할 수 없다.
비트맵 데이터에 직접 접근하고자 한다면 CoreGraphics나 CoreImage를 사용한다.
그리고 여기에 사용되는 이미지가 CGImage와 CIImage이다.

image?.cgImage
image?.ciImage

각각을 사용하려는 경우 위와 같이 cgImage와 ciimage 속성에 접근해 간단히 변환할 수 있다.

let data = image?.pngData()
let data2 = image?.jpegData(compressionQuality: 1.0)

이미지를 네트워크를 통해 전달하려면 binary 이미지로 변환해야 한다.
이런 동작도 image 내의 메서드로 간단히 변환 가능한데,
JPEG의 경우 0.0 ~ 1.0 사이의 압축 비율을 설정해야 한다.

 

Resizable Image & Vector Images


Resizable Image

Button의 background로 설정된 이미지가 버튼에 맞게 변형되면서 모서리가 지저분해졌다.

이러한 문제를 해결하는 방법 중 하나가 Resizable Image이다.
이미지에서 조정할 부분과 그대로 둘 부분을 지정하면 깔끔하게 표시할 수 있다.

asset 라이브러리는 image slicing 기능을 지원한다.
우측 하단의 Slicing을 선택하면 slicing 모드로 진입할 수 있다.

가운데의 resize 버튼을 누르면 resize 방식을 선택할 수 있다.
순서대로 좌우, 상하좌우, 상하 방향으로 늘어나며, 이미지가 단순하다면 지금처럼 자동으로 범위를 지정하기도 한다.

지금 storyboard는 이전과는 다르게 모서리가 깔끔하게 표현된 것을 볼 수 있다.
또한, 테두리의 두께도 일정하다.

slicing에서 나뉜 구역 중 밝게 보이는 네 모서리는 항상 고정된 비율로 표현된다.

따라서 지금처럼 글자의 일부분이 밝은 구역에 포함되도록 한다면 오른쪽 사진과 같이 기형적인 모습으로 나타난다.
더 정확히 구분하자면 밝은 부분 중 선과 선으로 이루어진 영역은 리사이징 되고, 어두운 부분은 무시된다.
또한, 리사이징 되는 구역은 기본적으로 타일 방식으로 반복되기 때문에 버튼에서도 해당 부분이 반복적으로 표현된다.

attribute inspector의 slicing > Center에는 이러한 부분들을 어떻게 표현할 것인지를 선택할 수 있다.

stretch와 tile은 이러한 차이가 있다.

또한 드래그가 아닌 픽셀 단위로 정확히 구역을 지정하고 싶다면, 해당 부분에서 slices 옵션에서 정확학 수치를 입력해 지정할 수도 있다.
각각에 대한 명칭은 Top, Bottom, Left, Right 다.

//
//  ResizeViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/17.
//

import UIKit

class ResizeViewController: UIViewController {
	@IBOutlet weak var btn: UIButton!
	
	let image = UIImage(named: "bbb")
	
	override func viewDidLoad() {
		super.viewDidLoad()
	
	}

}

씬과 코드의 연결을 위와 같다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if let imageSet = image {
		let inset = UIEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
		
		let resized = imageSet.resizableImage(withCapInsets: inset)
		
		btn.setBackgroundImage(resized, for: .normal)
	}

}

이미지를 resizable 이미지로 바꾸고, button의 background image로 설정한다.
먼저 UIEdgeInsets 메소드를 통해 inset을 설정한다.
위에서 설명한 것과 같이 left, right, top, bottom 네 개의 inset을 설정해 줘야 한다.

이후 resizableImage 메소드를 사용해 resizableImage를 생성해 반환한다.

그리고 button의 backgroundImage로 설정한다.


결과

 


테두리는 깔끔하지만 글자가 반복돼 표시되고 있다.

Asset 라이브러리에서 slicing 하면 원본 이미지에 직접 적용하지만,
코드를 통해 slicing하면 원본 이미지 대신 새로운 이미지를 생성한다.
또한, center 구역의 크기를 직접 지정하지 않고 inset으로 지정한 구역의 중간 전체를 center 구역으로 인식하게 된다.

resizeableImage 메서드는 이 center 구역의 크기에 따라 Center의 표현 방식을 결정하는데,
center 구역이 1*1 이상이면 tile을 자동으로 선택한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if let imageSet = image {
		let inset = UIEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
		
		let resized = imageSet.resizableImage(withCapInsets: inset, resizingMode: .stretch)
		
		btn.setBackgroundImage(resized, for: .normal)
	}

}

따라서 resizingMode 파라미터를 추가로 전달해 어떤 방식으로 resize 할 것인지를 지정해야 한다.


결과

 


새로운 이미지는 128px 크기에 2x스케일을 가지고 있다.
따라서 화면에는 64px로 표시된다.
또한 이미지가 복잡하기 때문에 resize 하기 애매한 부분이 있다.

 

Vector Image

왼쪽은 일반 PNG 파일을 원본 크기와 원본보다 큰 크기의 ImageView로 출력했을 때의 모습이다.
오른쪽은 Vector 파일을 원본 크기와 원본보다 큰 크기의 ImageView로 출력했을 때의 모습이다.

큰 ImageView에서 이미지의 품질 차이가 나타나는데 이렇게 resize를 사용할 수 없는 경우,
크기별로 여러 개의 이미지를 사용해 대응할 수도 있지만 Vector 파일을 사용하는 것으로 더욱 간단하게 해결할 수도 있다.

이러한 Vector 파일은 PDF Vector와 SVG Vector 두 가지가 존재하는데,
PDF Vector는 Xcode 6에서부터 사용됐으며, 거의 대부분의 버전에서 사용할 수 있을 만큼 호환성이 좋다.
단, 일반 이미지를 PDF Vector로 변환하는 과정이 조금 번거로운 단점이 있다.
SVG Vector는 Xcode 12와 iOS 13에서 도입됐고, 덕분에 호환성이 떨어진다는 단점이 있다.
대신 웹에서 널리 사용되는 파일인 만큼 파일 포맷으로 인한 문제는 없다.

벡터 이미지를 추가하는 경우 Resizing > Preserve Vertor Data를 활성화하고,
Scales를 Single Scale로 바꿔줘야 한다는 것을 명심하자.

 

Template Images


//
//  RenderingViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class RenderingViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    

    override func viewDidLoad() {
        super.viewDidLoad()
        }
    }
}

구성과 코드 연결은 위와 같다.
button과 imageView, barButton이 같은 이미지를 표시하도록 설정한 뒤 시뮬레이터를 확인하면
button과 imageView와 달리 barButton만 다른 방식으로 표시되는 것을 확인할 수 있다.

이는 barButton의 이미지를 표시할 때 template으로 표시하기 때문이다.
template 이미지는 원본 이미지에서 색상 정보를 모두 제거한 뒤, 불투명한 부분을 채워 넣게 된다.
따라 색상 정보가 지워지면서 디테일이 사라지고, 불투명한 부분을 모두 채워 넣어 그냥 단순한 사각형이 되어 버렸다.
template 방식은 디테일이 사라지고 단순해진다는 단점이 있지만, tintColor에 따라 색이 변하기 때문에,
테마에 맞게 적용할 수 있다는 장점이 있다.

asset에서 이미지를 선택한 뒤 Render As 옵션을 설정할 수 있다.
기본 값은 Default로 해당 설정은 이미지를 출력하는 컨트롤이 이미지를 출력하는 방식을 직접 선택하게 된다.
따라서 button과 imageView는 Original Image를 표시했고, barButton은 Template Image를 표시했다는 이야기다.


결과


따라서 해당 설정값을 바꾸어 이미지가 표시되는 방식을 강제로 지정할 수도 있다.

 

코드로 설정하기

override func viewDidLoad() {
	super.viewDidLoad()
	
	if let image = UIImage(named: "news")?.withRenderingMode(.alwaysTemplate) {
	
	}
}

먼저 원본 이미지를 불러온다.
이미지를 불러오는 UIImage 클래스에는 withRenderingMode 메서드가 존재하고,
해당 메소드에 alwaysTemplate, alwaysOriginal의 설정값을 전달할 수 있다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if let image = UIImage(named: "news")?.withRenderingMode(.alwaysTemplate) {
		imageView.image = image
	}
}

반환된 이미지를 imageView에 전달하면 된다.


결과


imageView로 표시되는 이미지가 template 방식으로 설정됐다.

앞서 말했듯, RootView의 tint를 변경해 template 이미지의 색상을 바꿀 수 있는 것은 장점이다.

 

Custom Drawing and Resizing


Custom Drawing

씬의 구성은 위와 같고,

씬과 씬 내부의 검은색 View는 각각의 커스텀 클래스를 가진다.
해당 View는 다시 흰색으로 설정했다.

//
//  CustomViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class CustomDrawingView: UIView {
	let star = UIImage(systemName: "star")
	let cloud = UIImage(systemName: "cloud")
	let phone = UIImage(systemName: "phone")
	
	override func draw(_ rect: CGRect) {
		super.draw(rect)
	}
}

class CustomViewController: UIViewController {
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}

검은색 View가 가지는 커스텀 클래스 CustomDrawingView는 위와 같이 씬의 코드에 선언되어있다.
해당 클래스는 별, 구름, 해 이미지를 불러온다.

customView를 구현할 때는 위와 같이 draw코드를 직접 구현해야 한다.
해당 메소드는 수명주기에 따라 반복적으로 호출되므로 새로 view를 생성할 수는 없다.
따라서 UIImage가 제공하는 방법을 통해 직접 구현해야 한다.

이미지를 그리는 방법은 세 가지이다.

  • 원하는 곳의 좌표를 전달하는 방법
    draw(at:) 메서드에 CGPoint로 좌표를 전달하면
override func draw(_ rect: CGRect) {
	super.draw(rect)
	star?.draw(at: CGPoint(x: 0, y: 0))
}

결과

 


사진과 같이 좌측 상단에 이미지가 그려진다.
지금처럼 크기를 지정하지 않으면 원본 크기로 그려지게 된다.

  • 크기를 지정하기
    draw(in:) 메소드를 사용한다.
    CGRect로 좌표와 크기를 전달하면
override func draw(_ rect: CGRect) {
	super.draw(rect)
	star?.draw(at: CGPoint(x: 0, y: 0))
	cloud?.draw(in: CGRect(x: 0, y: 100, width: 100, height: 100))
}

결과

 


사진과 같이 기본 크기보다 큰 크기로 화면에 표시된다.

  • 패턴 형식으로 그리기
    drawAsPattern 메서드이다.
    해당 메서드는 지정된 frame에 이미지를 그리는데, 이미지의 크기가 frame보다 작다면 pattern 형식으로 그린다.
    위처럼 View 전체의 frame을 전달하면
override func draw(_ rect: CGRect) {
	super.draw(rect)
	star?.draw(at: CGPoint(x: 0, y: 0))
	cloud?.draw(in: CGRect(x: 0, y: 100, width: 100, height: 100))
	phone?.drawAsPattern(in: rect)
}

결과

 


사진과 같이 전체 영역에 걸쳐서 이미지가 반복돼 표시된다.

 

Resizing

씬 구성은 화면 전체를 덮는 거대한 imageView와 거대한 사진이다.
사진이 너무 커 View안에 다 들어오지 않을 뿐만 아니라 ContentMode가 Center이기 때문에 사진의 중앙만 표시되고 있다.
이런 상황을 위해 Resizing 하게 되는데, 이는 두 가지 방법이 있다.

ImageContext

//
//  ImageResizeViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class ImageResizeViewController: UIViewController {
	@IBOutlet weak var imageView: UIImageView!
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
	
}

extension ImageResizeViewController {
	func resizeImageContext(image: UIImage, to size: CGSize) -> UIImage? {
		return nil
	}
}

첫 번째 방법은 ImageContext를 사용하는 방법이다.
선언되어 있는 메서드는 resizing할 이미지와 크기를 파라미터로 받는다.

사용할 메소드는 UIGraphicsBeginImageContextWithOptions로 imageContext를 생성한다.
imageContext는 그림판이라고 생각하면 편하다.

  • size
    context의 크기를 전달한다.
  • opaque
    이미지가 불투명하다면 true, 투명하다면 false를 전달한다.
  • scale
    대상 이미지의 스케일을 전달한다.
    0.0을 전달하면 기기의 스케일을 그대로 사용한다.
extension ImageResizeViewController {
    func resizeImageContext(image: UIImage, to size: CGSize) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
        
        let frame = CGRect(origin: CGPoint.zero, size: size)
        image.draw(in: frame)
        
        let result = UIGraphicsGetImageFromCurrentImageContext()
        
        UIGraphicsEndImageContext()
        return result
    }
}

Context를 생성했다면 자유롭게 해당 구역에 이미지를 그릴 수 있다.
frame 사이즈를 정의하고 해당 frame에 이미지를 그린 후,
UIGraphincsGetImageFromCurrentImageContext 메서드를 사용해 그려진 이미지를 추출한다.

반드시 UIGraphicsEndImageContext를 사용해 사용한 Context를 삭제하고, 결과를 반환한다.

override func viewDidLoad() {
	super.viewDidLoad()
	if let image = UIImage(named: "parrot") {
		let size = CGSize(width: image.size.width / 4, height: image.size.height / 4)
		
		imageView.image = resizeImageContext(image: image, to: size)
	}
}

이후엔 대상 이미지를 불러오고, 조절할 사이즈를 정한 다음 메소드를 호출해 주면 된다.


결과

 


조절된 이미지가 잘 표시된다.

Context 방식은 지교적 단순하게 구현할 수 있다는 장점이 있지만,
Context를 만들 때 사용할 수 있는 옵션이 제한적이라는 단점이 있다.

BitmapContext

//
//  ImageResizeViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit
import CoreGraphics

class ImageResizeViewController: UIViewController {
	@IBOutlet weak var imageView: UIImageView!
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
		if let image = UIImage(named: "parrot") {
			let size = CGSize(width: image.size.width / 4, height: image.size.height / 4)
			
			imageView.image = resizeImageBitmap(image: image, to: size)
		}
	}
}

extension ImageResizeViewController {
	func resizeImageBitmap(image: UIImage, to size: CGSize) -> UIImage? {
		return nil
	}
}

imageContext는 UIKit을 통해 제공되기 때문에 별도의 프레임워크를 추가할 필요가 없다.
하지만 BitMapContext는 CoreGraphics에 구현되어 있기 때문에 위와 같이 CoreGraphics를 import 해 줘야 한다.

extension ImageResizeViewController {
	func resizeImageBitmap(image: UIImage, to size: CGSize) -> UIImage? {
		guard let cgimage = image.cgImage else {
			return nil
		}
	}
}

imamgeContext 방식은 이미지를 처리할 때 UIImage를 사용했지만 CoreGraphics에서는 CoreGraphicsImage를 사용한다.
위와 같이 cgImage 속성을 사용하여 간단히 변경할 수 있다.

이후 BitmapContext를 만들기 위해 CGContext를 생성한다.
생성자에 전달되는 파라미터는 다음과 같다.

  • data
    랜더링 할 데이터의 포인터
    nil을 전달하면 자동으로 처리한다.
  • width, height
    이미지의 높이와 너비를 전달한다.
    Int의 형식이기 때문에 CGSize를 변환해 줘야 한다.
  • bitsPerComponent, bytesPerRow, space, bitmap Into
    네 가지 속성은 Bitmap에 관한 속성이다.
    Bitmap에 관한 이해를 건너뛰고, 기존 이미지의 설정을 그대로 사용한다.
func resizeImageBitmap(image: UIImage, to size: CGSize) -> UIImage? {
	guard let cgimage = image.cgImage else {
		return nil
	}
	
	let bmc = cgimage.bitsPerComponent
	let bmr = cgimage.bytesPerRow
	let bms = cgimage.colorSpace!
	let bmi = cgimage.bitmapInfo
	
	guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: bmc, bytesPerRow: bmr, space: bms, bitmapInfo: bmi.rawValue) else {
		return nil
	}

}

위와 같이 전달된 이미지의 속성들을 추출해 전달한다.
bitmapInfo의 경우 원시 값을 전달해야 함을 알아두자.

func resizeImageBitmap(image: UIImage, to size: CGSize) -> UIImage? {
	guard let cgimage = image.cgImage else {
		return nil
	}
	
	let bmc = cgimage.bitsPerComponent
	let bmr = cgimage.bytesPerRow
	let bms = cgimage.colorSpace!
	let bmi = cgimage.bitmapInfo
	
	guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: bmc, bytesPerRow: bmr, space: bms, bitmapInfo: bmi.rawValue) else {
		return nil
	}
	
	context.interpolationQuality = .high

}

CGContext에는 interpolationQuality라는 속성이 존재한다.
해당 속성은 리사이징 시의 이미지 품질을 결정하는데, 위의 코드처럼 high로 설정하면 품질 손실을 최소화할 수 있다.

extension ImageResizeViewController {
	func resizeImageBitmap(image: UIImage, to size: CGSize) -> UIImage? {
		guard let cgimage = image.cgImage else {
			return nil
		}
	
		let bmc = cgimage.bitsPerComponent
		let bmr = cgimage.bytesPerRow
		let bms = cgimage.colorSpace!
		let bmi = cgimage.bitmapInfo
	
		guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: bmc, bytesPerRow: bmr, space: bms, bitmapInfo: bmi.rawValue) else {
			return nil
		}
		
		context.interpolationQuality = .high
		
		let frame = CGRect(origin: .zero, size: size)
		
		context.draw(cgimage, in: frame)
		
		guard let result = context.makeImage() else {
			return nil
		}
		
		return UIImage(cgImage: result)
	}
}

이후엔 frame을 지정하고, 이미지를 그린 뒤 만들어진 이미지를 UIImage로 변환해 전달한다.
이때 ImageContext와는 다르게 makeImage 메소드를 사용하고, UIImage 생성자를 사용해 UIImage로 변환하는 것을 기억하자.

override func viewDidLoad() {
	super.viewDidLoad()
	if let image = UIImage(named: "parrot") {
		let size = CGSize(width: image.size.width / 4, height: image.size.height / 4)
		
		imageView.image = resizeImageBitmap(image: image, to: size)
	}
}

이후엔 해당 메소드를 호출해 이미지를 표시한다.


결과

 


동일하게 크기가 조절되었다.

크기를 조절하면서 bitmap 속성을 함께 설정해야 한다면 bitmapContext 방식을 사용하고,
이외의 경우엔 imageContext 속성을 사용한다.

이 외에도 다양한 방식의 resizing 방법이 존재하고, Low Level API를 사용해 더 빠르게 변환할 수도 있다.

 

Color Basic


Interface Builder에서 바꾸기

interface Builder에서의 색 설정은 간단하다.
attribute inspector에 표시되는 background, tint 속성이 View의 색을 결정하는 속성이다.
미리보기와 함께 보여주기 때문에 직관적이다.

더보기를 선택하면 현재 색상, 최근에 사용한 색상, named 색상, 시스템 색상을 순서대로 보여준다.

 

Code에서 바꾸기

//
//  ColorViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class ColorViewController: UIViewController {
	@IBOutlet weak var colorView: UIView!
	@IBOutlet weak var sliderR: UISlider!
	@IBOutlet weak var sliderG: UISlider!
	@IBOutlet weak var sliderB: UISlider!
	
	
	@IBAction func sliderAction(_ sender: Any) {
	
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}

사용할 씬과 연결된 코드는 위와 같다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	colorView.backgroundColor = UIColor.black
}

연결된 view의 backgroundColor 속성에 설정할 색을 전달한다.

해당 속성은 UIColor 형식을 사용하며, UIKit을 통해 제공되는 대부분의 API들의 색상 관련 속성들은 대부분  UIColor를 사용한다.

UIColor는 위와 같이 자주 사용하는 색상을 이름으로 제공하기도 한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	colorView.backgroundColor = UIColor.black
}

이렇게 되면 view의 backgroundColor가 검은색으로 설정된다.

또한 UIColor는 생성자를 통해 여러 방식의 색상을 직접 지정할 수 있도록 제공한다.

이 중 RGB를 사용하는 경우 일반적인 0 ~ 255 사이의 값의 RGB코드를 사용하는 대신,
UIColor에서 사용하는 0.0 ~ 1.0 사이의 값으로 변환해서 전달해야 한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	colorView.backgroundColor = UIColor.black
	
	let newColor = UIColor(red: 100, green: 100, blue: 100, alpha: 1.0)
}

따라서 이와 같이 정수로 전달하는 것이 아닌

override func viewDidLoad() {
	super.viewDidLoad()
	
	colorView.backgroundColor = UIColor.black
	
	let newColor = UIColor(red: 100.0 / 255.0, green: 100.0 / 255.0, blue: 100.0 / 255.0, alpha: 1.0)
}

이렇게 최댓값이 255로 나눈 값을 전달해야 한다.

생성자 중 displayP3로 시작하는 생성자는 P3 색욕의 색상을 만들 수 있다.

UIColor(displayP3Red: 100.0 / 255.0, green: 100.0 / 255.0, blue: 100.0 / 255.0, alpha: 1.0)

이러한 P3 색역을 sRGB 대비 25% 대비 더 넓은 색 역대를 가지고 있다.
해당 색 역대는 현행 애플이 사용하는 디스플레이 표준색 역대로 이전 기기들에 비해 더 많은 색을 표현할 수 있다.
따라서 기존의 RGB보다는 P3대역을 사용해 만드는 것이 좋다.

이렇게 한 번 만들어진 색상은 이후엔 조절할 수 없다.
따라서 조금의 수정이 필요하다면 다른 색상을 추가로 만들어야 하지만 투명도에 해당하는 alpha는 조금 다르다.

newColor.withAlphaComponent(0.2)
UIColor.clear

위의 코드와 같이 withAlphaComponent를 사용해 alpha값을 조절하거나,
alpha 0.0의 색상이 필요하다면 clear 속성을 통해 투명하게 표현할 수 있다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	colorView.backgroundColor = UIColor.black
	
	let newColor = UIColor(displayP3Red: 100.0 / 255.0, green: 100.0 / 255.0, blue: 100.0 / 255.0, alpha: 1.0)
	
	sliderR.value = 100.0 / 255.0
	sliderG.value = 100.0 / 255.0
	sliderB.value = 100.0 / 255.0
	
	colorView.backgroundColor = newColor
}

이제 새로운 색으로 View를 초기화하고,
slider의 값들을 동기화해 준다.

override func viewDidLoad() {
	super.viewDidLoad()
	var r = CGFloat(0)
	var g = CGFloat(0)
	var b = CGFloat(0)
	var a = CGFloat(0)

	colorView.backgroundColor = UIColor.black

	let newColor = UIColor(displayP3Red: 100.0 / 255.0, green: 100.0 / 255.0, blue: 100.0 / 255.0, alpha: 1.0)
	
	newColor.getRed(&r, green: &g, blue: &b, alpha: &a)
	
	sliderR.value = Float(r)
	sliderG.value = Float(g)
	sliderB.value = Float(b)
	
	colorView.backgroundColor = newColor
}

이후 slider의 값이 변경될 때마다 각 슬라이더의 값들을 읽어와 새로운 컬러로 합친 뒤 View의 backGround 색을 변경한다.
이미 존재하는 색에 접근해 값을 구해 올 때는 CGFloat 형식의 인스턴스에 getRed 메서드에 포인트로 전달해 값을 저장하도록 한다.
이후 슬라이더에 전달할 때는 다시 Float으로 변환해 줘야 한다.


결과

 


CGColor와 CIColor

//
//  CGCIViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class CGCIViewController: UIViewController {
	@IBOutlet weak var colorView: UIView!
	
	override func viewDidLoad() {
		super.viewDidLoad()
	
		colorView.layer.borderWidth = 10
	}
}

사용할 씬과 연결된 코드는 위와 같다.
viewDidLoad에서 layer.borderWidth를 수정해 테두리의 두께를 조절했다.

해당 테두리의 색을 바꾸려면 borderColor 속성에 접근해야 하고,
borderColor는 위와 같이 CGColor형식을 사용한다. CGColor는 색을 섬세하게 다룰 때 주로 사용하며,

UIColor처럼 색을 제공하지도 않아 직접 만들어 사용해야 한다.
그래서 보통은 UIColor의 색을 CGColor로 변환해 사용한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	colorView.layer.borderWidth = 10
	colorView.layer.borderColor = UIColor.systemYellow.cgColor
}

이렇게 기존에 존재하는 UIColor의 cgColor 속성을 사용해 쉽게 CGColor로 바꿀 수 있다.

비슷한 이름의 ciColor도 UIColor를 CIColor로 바꾸는 데 사용한다.
이 CIColor는 주로 필터 효과를 구현할 때 사용한다.


결과

 


반대로 CGColor나 CIColor를 UIColor로 바꿀 때는 위와 같이 UIColor 생성자를 사용한다.

 

Pattern Color, Color Literal, Color Set


Pattern Color

//
//  ETCViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class ETCViewController: UIViewController {
	
	override func viewDidLoad() {
	super.viewDidLoad()
}

사용할 씬과 연결된 코드는 위와 같다.

UIColor는 이름 상 단색의 느낌이지만 위와 같이 이미지를 받는 생성자가 존재한다.

//
//  ETCViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/18.
//

import UIKit

class ETCViewController: UIViewController {
	
	override func viewDidLoad() {
	super.viewDidLoad()
	
	if let image = UIImage(named: "pattern") {
		let pattern = UIColor(patternImage: image)
		view.backgroundColor = pattern
		}
	}
}

해당 생성자를 사용해 UIColor를 만들고 backgroundColor로 설정해 봤다.


결과


이렇게 영역 전체를 pattern으로 덮게 된다.
만약 패턴의 크기를 바꾸고 싶다면 패턴으로 사용할 이미지의 크기를 조절해 줘야 한다.

 

Color Literal

//
//  ColorSetViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/19.
//

import UIKit

class ColorSetViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}

사용할 씬과 연결된 코드는 위와 같다.

이번엔 Color Literal로 색을 만들어 본다.

color의 자동완성에는 Color Literal이 존재한다.
이를 선택하면 오른쪽과 같이 흰 사각형이 코드에 하나 생기는데, 이것이 Color Literal이다.
Color Literal은 상수에 저장할 수도 있고, 직접 전달될 수도 있다.

더블클릭하면 이렇게 색을 선택할 수 있는 팝업이 등장한다.

 

ColorSet

asset에는 보통 이미지를 추가하지만 사용자가 원하는 ColorSet을 추가할 수도 있다.

asset에서 우클릭으로 Color Set을 추가할 수 있는데, 선택 화면 위와 같은 설정 창이 생기게 되고,
각각의 사각형은 Color Well이라고 부르고, 기본 모드와 다크 모드에서 사용할 색을 각각 설정할 수 있다.

Color Well을 선택한 다음 inspector에서 show Color Panel을 통해 색을 설정할 수 있다.

이후엔 뷰의 BackgroundColor 설정의 namedColor나 코드에서 사용할 수 있다.

class ColorSetViewController: UIViewController {

	override func viewDidLoad() {
		super.viewDidLoad()
		
		let c = #colorLiteral(red: 1, green: 0.5287722349, blue: 0, alpha: 1)
	
		view.backgroundColor = UIColor(named: "color") ?? UIColor.systemRed
	}
}

코드에서 사용할 때는 UIColor(named:) 생성자를 사용한다.
이때 시스템의 환경에 맞춰 lightmode와 darkmode의 색을 알아서 사용한다.
전달하는 이름은 대소문자까지 완벽히 구분하고, 만약 해당 이름의 ColorSet이 존재하지 않는다면 nil을 반환한다.
그렇기 때문에 항상 기본색을 함께 전달하는 것이 좋다.


결과


화면에 다크 모드와 라이트 모드에서 알맞은 색이 표시되고 있다.

 

Cutomdraw 색 설정

사용할 씬은 위와 같고, 씬에 포함된 View는 커스텀 class를 갖는다.

//
//  CustomColorViewViewController.swift
//  ImageTest
//
//  Created by Martin.Q on 2021/08/19.
//

import UIKit



class CustomView: UIView {
	override func draw(_ rect: CGRect) {
		let context = UIGraphicsGetCurrentContext()

		var frame = CGRect(x: 10, y: 10, width: 100, height: 100)
		context?.addRect(frame)
		context?.strokePath()
		
		frame = CGRect(x: 10, y: 200, width: 100, height: 100)
		context?.addRect(frame)
		context?.fillPath()
	}
}

class CustomColorViewViewController: UIViewController {

	override func viewDidLoad() {
		super.viewDidLoad()
	}
}

위와 같이 draw를 사용해 view 안에 CustomDraw를 구현한다.


결과

 


위와 같이 검은색으로 그려지게 된다.
이는 CustomDraw를 구현하면서 색을 지정하지 않았기 때문으로, 그리기에 사용할 색을 지정하면 더 다채롭게 표현할 수 있다.

class CustomView: UIView {
	override func draw(_ rect: CGRect) {
		let context = UIGraphicsGetCurrentContext()
		
		var frame = CGRect(x: 10, y: 10, width: 100, height: 100)
		context?.addRect(frame)
		UIColor.systemRed.setStroke()
		context?.strokePath()
		
		frame = CGRect(x: 10, y: 200, width: 100, height: 100)
		context?.addRect(frame)
		context?.fillPath()
	}
}

UIColor의 setStroke 메서드를 사용하면 해당 색으로 stroke의 색이 설정된다.
해당 설정은 다른 색으로 지정되기 전까지 유지된다.

class CustomView: UIView {
	override func draw(_ rect: CGRect) {
	let context = UIGraphicsGetCurrentContext()
		
		var frame = CGRect(x: 10, y: 10, width: 100, height: 100)
		context?.addRect(frame)
		UIColor.systemRed.setStroke()
		context?.strokePath()
		
		frame = CGRect(x: 10, y: 200, width: 100, height: 100)
		context?.addRect(frame)
		UIColor.systemBlue.setFill()
		context?.fillPath()
	}
}

setFill 메소드를 사용하면 채우기 색으로 설정된다.


결과