Generic
- 타입에 유연하게 대응하기 위한 요소.
- 제네릭으로 구현한 타입과 기능은 재사용하기 쉽고 코드의 중복도 줄일 수 있기 때문에 깔끔하고 추상적인 표현이 가능.
- T: Type Parameters로 플레이스 홀더와 같은 역할을 함.
- 타입의 종류는 알려주지 않지만 특정한 타입이라는 것은 알려줌.
- T가 아닌 다른 문자열을 작성해도 되지만, 일반적으로는 UpperCased 또는 T, U 등을 사용함.
🔥 만약 함수를 오버로딩하는데 제네릭을 사용한 경우, 제네릭으로 구현한 함수보다 타입이 명확한 함수가 우선순위가 더 높다.
🔥 프로토콜에서 제네릭을 사용하려면 associated type 을 사용해야 한다.
protocol GenericExample {
associatedtype T
var variable: T { get }
}
class A: GenericExample {
var variable: Int { return 100 }
}
struct B: GenericExample {
var variable: String { return "BBB" }
}
protocol GenericExample {
associatedtype T: Numeric
// Numeric: 곱셈을 지원하는 프로토콜
var variable: T { get }
}
class A: GenericExample {
var variable: Int { return 100 }
}
struct B: GenericExample {
var variable: Double { return 5.5 }
}
// 다음은 틀린코드 -> String은 Numeric을 충족하지 않으므로 타입으로 들어올 수 없다.
struct C: GenericExample {
var variable: String { return "CCC" }
}
제네릭을 활용한 UI Design 메서드 구현
import UIKit
extension UIViewController {
// Type Constraints: 클래스 제약, 프로토콜 제약
func configureBorder<T: UIView>(view: T) {
view.layer.cornerRadius = 10.0
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 3.0
view.layer.masksToBounds = true
}
}
제네릭을 활용한 화면 전환 메서드
extension UIViewController {
enum TransitionStyle {
case present // 네비게이션 없이 present
case presentNavigation // 네비게이션 임베드 된 present
case presentFullNavigation // 네비게이션 임베드 된 fullscreen present
case push
}
func transition<T: UIViewController>(
viewController: T.Type,
storyboard: String,
style: TransitionStyle
) {
let sb = UIStoryboard(name: storyboard, bundle: nil)
guard let vc = sb.instantiateViewController(
withIdentifier: String(describing: viewController)
) as? T else { return }
switch style {
case .present:
present(vc, animated: true)
case .presentNavigation:
let nav = UINavigationController(rootViewController: vc)
present(nav, animated: true)
case .presentFullNavigation:
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
case .push:
navigationController?.pushViewController(vc, animated: true)
}
}
}
/* 사용 예
transition(
viewController: GenericViewController.self,
storyboard: "Main",
style: .presentNavigation
)
*/
required init
- 초기화 구문이 프로토콜에 명시되어 있음을 알려주는 키워드.
- 스토리보드를 안쓰면 항상 추가해줘야 함.
// 프로토콜을 통해 init 강제
protocol ExampleProtocol {
init(name: String)
}
class Mobile: ExampleProtocol {
// required: 프로토콜에서 생성된 경우 사용하는 키워드
// Required Initializer
required init(name: String) {
fatalError()
}
}
CustomView에서 CornerRadius 적용하기(feat. layoutSubViews)
- UI 컴포넌트의 width를 2로 나눈 값을 cornerRadius로 주면, 원이 된다.
import UIKit
class CustomImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureView() {
backgroundColor = .systemMint
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 2
layer.cornerRadius = frame.width / 2
clipsToBounds = true
}
}
위에서 구현한 CustomImageView 타입을 실제로 뷰컨트롤러에서 사용해보면,
class ViewController: UIViewController {
private lazy var customImageView: UIImageView = {
let imageView = CustomImageView(frame: .zero)
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configureUI()
}
func configureUI() {
view.addSubview(customImageView)
customImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
customImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
customImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
customImageView.widthAnchor.constraint(equalToConstant: 150),
customImageView.heightAnchor.constraint(equalToConstant: 150)
]
)
}
}
하지만 막상 실행하면 cornerRadius만 적용이 안된 것을 볼 수 있다.

사용하는 곳에서 이 이미지뷰를 초기화할 때, frame: .zero 로 했기 때문에 frame.width = 0이라서 반영되지 않았던 것!
⭐️⭐️ 해결방법: layoutSubviews()에서 frame 값이 필요한 부분을 작업하면 된다.
layoutSubviews
- UIView의 메서드로, 서브뷰들의 레이아웃을 업데이트한다.
- 불리는 시점
1. 뷰컨트롤러가 메모리에 로드된다(viewDidLoad)
2. viewWillAppear가 호출
3. 뷰컨트롤러의 뷰들이 레이아웃 제약들을 업데이트하고 intrinsicContentSize를 계산한다(updateViewConstraints)
✅ 4. viewWillLayoutSubviews가 호출되면, 뷰들은 layoutSubviews를 호출
5. viewDidLayoutSubviews가 호출되고, 화면에 뷰들을 그린다(drawRect)
6. viewDidAppear 호출
import UIKit
class CustomImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureView() {
backgroundColor = .systemMint
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 2
clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.width / 2
}
}

UIPageViewController(feat. Intro, Walkthroungh, Onboarding)
- 여러개의 뷰 컨트롤러를 페이징이 가능한 하나의 뷰 컨트롤러로 관리해주는 컨테이너 뷰 컨트롤러.
- UIPageViewControllerDataSource와 UIPageViewControllerDelegate 를 통해 페이지뷰컨트롤러의 기능을 구현해야함.
전체 코드
1. PageViewController에 담을 UIViewController 구현
import UIKit
class FirstVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .brown
}
}
class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemMint
}
}
class ThirdVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .magenta
}
}
2. PageViewController 구현
import UIKit
class OnboardingViewController: UIPageViewController {
// 1.
var list: [UIViewController] = []
// 화면전환 방식, 방향 설정
override init(
transitionStyle style: UIPageViewController.TransitionStyle,
navigationOrientation: UIPageViewController.NavigationOrientation,
options: [UIPageViewController.OptionsKey : Any]? = nil
) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad() {
super.viewDidLoad()
list = [FirstVC(), SecondVC(), ThirdVC()]
view.backgroundColor = .systemPink
delegate = self
dataSource = self
// 초기 ViewController 등록
guard let first = list.first else { return }
setViewControllers([first], direction: .forward, animated: true)
}
}
3. PageViewControllerDatasource 구현
- viewControllerBefore : 왼쪽으로 스크롤시, 이전 화면이 보이도록 구현
- viewControllerAfter : 오른쪽으로 스크롤시, 다음 화면이 보이도록 구현
extension OnboardingViewController: UIPageViewControllerDataSource {
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let currentIndex = list.firstIndex(of: viewController) else { return nil }
let previousIndex = currentIndex - 1
return previousIndex < 0 ? nil : list[previousIndex]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let currentIndex = list.firstIndex(of: viewController) else { return nil }
let nextIndex = currentIndex + 1
return nextIndex >= list.count ? nil : list[nextIndex]
}
}
4. PageViewControllerDelegate 구현(PageControl 추가)
- presentationCount : UIPageViewController의 UIPageControl에 표시할 갯수
- presentationIndex : 현재 페이지임을 표시할 UIPageControl의 인덱스
extension OnboardingViewController: UIPageViewControllerDelegate {
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return list.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let first = viewControllers?.first,
let index = list.firstIndex(of: first)
else { return 0 }
return index
}
}
UIPageViewController에 내장된 PageControl은 색상 등의 커스터마이징이 힘들다.
따라서 PageControl 속성값에 변화를 주고 싶으면 a) 직접 UIPageControl을 추가해서 변경해야한다.
이경우 a-1) PageControl의 위치 제약, a-2) PageControl의 아이템 갯수와 화면 스와이프시 PageControl CurrentIndex 반영을 직접 구현해줘야 한다.
다른 방법도 있는데, b) UIPageControl의 appearance()를 이용하면 된다.(proxy)
다만 이 방법은 프로젝트의 모든 UIPageControl에 전역적으로 적용되니, 좋은 방법은 아닐 수 있음.
페이지 뷰 컨트롤러 무한 스크롤 구현
계산한 이전(다음) 인덱스 값에서 뷰컨트롤러 갯수만큼 더하고, 이를 다시 뷰컨트롤러 갯수로 나눈 나머지값을 반환하면 된다.
extension OnboardingViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let firstIndex = viewControllerList.firstIndex(of: viewController) else { return nil }
let prevIndex = (firstIndex - 1 + viewControllerList.count) % viewControllerList.count
return prevIndex < 0 ? viewController : viewControllerList[prevIndex]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let firstIndex = viewControllerList.firstIndex(of: viewController) else { return nil }
let afterIndex = (firstIndex + 1) % viewControllerList.count
return afterIndex >= viewControllerList.count ? nil : viewControllerList[afterIndex]
}
}
'SeSAC' 카테고리의 다른 글
8.23 TIL(SeSAC iOS 3기), MapKit + frozen Enumeration (1) | 2023.08.27 |
---|---|
8.23 TIL(SeSAC iOS 3기), CoreLocation (0) | 2023.08.27 |
8.21 ~ 8.22 TIL(SeSAC iOS 3기) (0) | 2023.08.27 |
8.14 ~ 8.18 TIL (1) | 2023.08.22 |
8.7 ~ 8.11 TIL (0) | 2023.08.15 |
Generic
- 타입에 유연하게 대응하기 위한 요소.
- 제네릭으로 구현한 타입과 기능은 재사용하기 쉽고 코드의 중복도 줄일 수 있기 때문에 깔끔하고 추상적인 표현이 가능.
- T: Type Parameters로 플레이스 홀더와 같은 역할을 함.
- 타입의 종류는 알려주지 않지만 특정한 타입이라는 것은 알려줌.
- T가 아닌 다른 문자열을 작성해도 되지만, 일반적으로는 UpperCased 또는 T, U 등을 사용함.
🔥 만약 함수를 오버로딩하는데 제네릭을 사용한 경우, 제네릭으로 구현한 함수보다 타입이 명확한 함수가 우선순위가 더 높다.
🔥 프로토콜에서 제네릭을 사용하려면 associated type 을 사용해야 한다.
protocol GenericExample {
associatedtype T
var variable: T { get }
}
class A: GenericExample {
var variable: Int { return 100 }
}
struct B: GenericExample {
var variable: String { return "BBB" }
}
protocol GenericExample {
associatedtype T: Numeric
// Numeric: 곱셈을 지원하는 프로토콜
var variable: T { get }
}
class A: GenericExample {
var variable: Int { return 100 }
}
struct B: GenericExample {
var variable: Double { return 5.5 }
}
// 다음은 틀린코드 -> String은 Numeric을 충족하지 않으므로 타입으로 들어올 수 없다.
struct C: GenericExample {
var variable: String { return "CCC" }
}
제네릭을 활용한 UI Design 메서드 구현
import UIKit
extension UIViewController {
// Type Constraints: 클래스 제약, 프로토콜 제약
func configureBorder<T: UIView>(view: T) {
view.layer.cornerRadius = 10.0
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 3.0
view.layer.masksToBounds = true
}
}
제네릭을 활용한 화면 전환 메서드
extension UIViewController {
enum TransitionStyle {
case present // 네비게이션 없이 present
case presentNavigation // 네비게이션 임베드 된 present
case presentFullNavigation // 네비게이션 임베드 된 fullscreen present
case push
}
func transition<T: UIViewController>(
viewController: T.Type,
storyboard: String,
style: TransitionStyle
) {
let sb = UIStoryboard(name: storyboard, bundle: nil)
guard let vc = sb.instantiateViewController(
withIdentifier: String(describing: viewController)
) as? T else { return }
switch style {
case .present:
present(vc, animated: true)
case .presentNavigation:
let nav = UINavigationController(rootViewController: vc)
present(nav, animated: true)
case .presentFullNavigation:
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
case .push:
navigationController?.pushViewController(vc, animated: true)
}
}
}
/* 사용 예
transition(
viewController: GenericViewController.self,
storyboard: "Main",
style: .presentNavigation
)
*/
required init
- 초기화 구문이 프로토콜에 명시되어 있음을 알려주는 키워드.
- 스토리보드를 안쓰면 항상 추가해줘야 함.
// 프로토콜을 통해 init 강제
protocol ExampleProtocol {
init(name: String)
}
class Mobile: ExampleProtocol {
// required: 프로토콜에서 생성된 경우 사용하는 키워드
// Required Initializer
required init(name: String) {
fatalError()
}
}
CustomView에서 CornerRadius 적용하기(feat. layoutSubViews)
- UI 컴포넌트의 width를 2로 나눈 값을 cornerRadius로 주면, 원이 된다.
import UIKit
class CustomImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureView() {
backgroundColor = .systemMint
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 2
layer.cornerRadius = frame.width / 2
clipsToBounds = true
}
}
위에서 구현한 CustomImageView 타입을 실제로 뷰컨트롤러에서 사용해보면,
class ViewController: UIViewController {
private lazy var customImageView: UIImageView = {
let imageView = CustomImageView(frame: .zero)
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configureUI()
}
func configureUI() {
view.addSubview(customImageView)
customImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
customImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
customImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
customImageView.widthAnchor.constraint(equalToConstant: 150),
customImageView.heightAnchor.constraint(equalToConstant: 150)
]
)
}
}
하지만 막상 실행하면 cornerRadius만 적용이 안된 것을 볼 수 있다.

사용하는 곳에서 이 이미지뷰를 초기화할 때, frame: .zero 로 했기 때문에 frame.width = 0이라서 반영되지 않았던 것!
⭐️⭐️ 해결방법: layoutSubviews()에서 frame 값이 필요한 부분을 작업하면 된다.
layoutSubviews
- UIView의 메서드로, 서브뷰들의 레이아웃을 업데이트한다.
- 불리는 시점
1. 뷰컨트롤러가 메모리에 로드된다(viewDidLoad)
2. viewWillAppear가 호출
3. 뷰컨트롤러의 뷰들이 레이아웃 제약들을 업데이트하고 intrinsicContentSize를 계산한다(updateViewConstraints)
✅ 4. viewWillLayoutSubviews가 호출되면, 뷰들은 layoutSubviews를 호출
5. viewDidLayoutSubviews가 호출되고, 화면에 뷰들을 그린다(drawRect)
6. viewDidAppear 호출
import UIKit
class CustomImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureView() {
backgroundColor = .systemMint
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 2
clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.width / 2
}
}

UIPageViewController(feat. Intro, Walkthroungh, Onboarding)
- 여러개의 뷰 컨트롤러를 페이징이 가능한 하나의 뷰 컨트롤러로 관리해주는 컨테이너 뷰 컨트롤러.
- UIPageViewControllerDataSource와 UIPageViewControllerDelegate 를 통해 페이지뷰컨트롤러의 기능을 구현해야함.
전체 코드
1. PageViewController에 담을 UIViewController 구현
import UIKit
class FirstVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .brown
}
}
class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemMint
}
}
class ThirdVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .magenta
}
}
2. PageViewController 구현
import UIKit
class OnboardingViewController: UIPageViewController {
// 1.
var list: [UIViewController] = []
// 화면전환 방식, 방향 설정
override init(
transitionStyle style: UIPageViewController.TransitionStyle,
navigationOrientation: UIPageViewController.NavigationOrientation,
options: [UIPageViewController.OptionsKey : Any]? = nil
) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad() {
super.viewDidLoad()
list = [FirstVC(), SecondVC(), ThirdVC()]
view.backgroundColor = .systemPink
delegate = self
dataSource = self
// 초기 ViewController 등록
guard let first = list.first else { return }
setViewControllers([first], direction: .forward, animated: true)
}
}
3. PageViewControllerDatasource 구현
- viewControllerBefore : 왼쪽으로 스크롤시, 이전 화면이 보이도록 구현
- viewControllerAfter : 오른쪽으로 스크롤시, 다음 화면이 보이도록 구현
extension OnboardingViewController: UIPageViewControllerDataSource {
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let currentIndex = list.firstIndex(of: viewController) else { return nil }
let previousIndex = currentIndex - 1
return previousIndex < 0 ? nil : list[previousIndex]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let currentIndex = list.firstIndex(of: viewController) else { return nil }
let nextIndex = currentIndex + 1
return nextIndex >= list.count ? nil : list[nextIndex]
}
}
4. PageViewControllerDelegate 구현(PageControl 추가)
- presentationCount : UIPageViewController의 UIPageControl에 표시할 갯수
- presentationIndex : 현재 페이지임을 표시할 UIPageControl의 인덱스
extension OnboardingViewController: UIPageViewControllerDelegate {
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return list.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let first = viewControllers?.first,
let index = list.firstIndex(of: first)
else { return 0 }
return index
}
}
UIPageViewController에 내장된 PageControl은 색상 등의 커스터마이징이 힘들다.
따라서 PageControl 속성값에 변화를 주고 싶으면 a) 직접 UIPageControl을 추가해서 변경해야한다.
이경우 a-1) PageControl의 위치 제약, a-2) PageControl의 아이템 갯수와 화면 스와이프시 PageControl CurrentIndex 반영을 직접 구현해줘야 한다.
다른 방법도 있는데, b) UIPageControl의 appearance()를 이용하면 된다.(proxy)
다만 이 방법은 프로젝트의 모든 UIPageControl에 전역적으로 적용되니, 좋은 방법은 아닐 수 있음.
페이지 뷰 컨트롤러 무한 스크롤 구현
계산한 이전(다음) 인덱스 값에서 뷰컨트롤러 갯수만큼 더하고, 이를 다시 뷰컨트롤러 갯수로 나눈 나머지값을 반환하면 된다.
extension OnboardingViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let firstIndex = viewControllerList.firstIndex(of: viewController) else { return nil }
let prevIndex = (firstIndex - 1 + viewControllerList.count) % viewControllerList.count
return prevIndex < 0 ? viewController : viewControllerList[prevIndex]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let firstIndex = viewControllerList.firstIndex(of: viewController) else { return nil }
let afterIndex = (firstIndex + 1) % viewControllerList.count
return afterIndex >= viewControllerList.count ? nil : viewControllerList[afterIndex]
}
}
'SeSAC' 카테고리의 다른 글
8.23 TIL(SeSAC iOS 3기), MapKit + frozen Enumeration (1) | 2023.08.27 |
---|---|
8.23 TIL(SeSAC iOS 3기), CoreLocation (0) | 2023.08.27 |
8.21 ~ 8.22 TIL(SeSAC iOS 3기) (0) | 2023.08.27 |
8.14 ~ 8.18 TIL (1) | 2023.08.22 |
8.7 ~ 8.11 TIL (0) | 2023.08.15 |