007 ~ 008 강의는 Xcode9 -> Xcode11 마이그레이션 강의로 따로 정리하지 않는다.
View & Window
Window(윈도우)와 View는 디바이스의 화면과 UI를 출력하고, 이벤트를 처리한다.
모든 앱은 적어도 하나 이상의 윈도우를 가지고, 외부 디스플레이가 연결되면 두 개의 윈도우를 가진다.
Window
터치 이벤트를 올바른 대상에 전달한다.
화면에 표시되는 뷰의 Controller 역할을 수행한다.
interface builder를 사용하면 대부분의 과정이 자동으로 진행되지만,
그 외의 방식으로 구성하는 경우 윈도우를 직접 생성하고, 뷰를 직접 추가해야 한다.
새로운 화면으로 전환하는 경우, 윈도우에 추가되어있는 뷰를 다른 뷰로 대체하는 방식으로 진행한다.
View
앱에서 시각적으로 표현되는 모든 것을 View(뷰)라고 부른다.
뷰는 크게 세 가지의 역할을 한다.
- Displaying Contents
출력 - Handling Touch Eventes
Frame(프레임) 내의 TouchEvent, 뷰의 상태를 확인하거나 업데이트한다. - Laying Out Subviews
자신에게 속하는 view들을 관리한다.
애플에서 제공하는 SystemView(시스템뷰)는
TextView, ControlView, ContentView, ContainerView, BarView
가 있으며, 이를 구성하는 각각은 다음과 같다.
TextView
- Label
정적인 text
- TextField
Single-line의 text
- TextView
수직 스크롤을 지원하는 Multi-line text
Control
- Button
터치 이벤트
- Switch
On/Off
- Slider
슬라이더
- PageControl
페이지 구성의 화면에서 페이지를 표현
- DatePicker
날짜 입력
- SeagmentedControl
여러 옵션 중 선택해야 할 때
- Stepper
숫자의 증가, 감소
Content
- ImageView
PNG나 JPG
- PickerView
옵션을 휠 방식으로 선택
- ProgressView
한정된 시간 동안 진행되는 경우
- ActivityIndicatorView
작업 완료 시간을 알 수 없는 경우 사용
- WebView
앱 내에서 웹 페이지 표시
- MapView
앱 내에서 지도 표시
Container
- ScrollView
한 화면에 들어오지 않는 컨텐츠의 드래그, 확대, 축소를 가능하게 함.
- TableView
목록을 수직으로 나열
- CollectionView
목록을 그리드 형식으로 나열
스크롤 방향과 레이아웃 설정이 가능
Bar
- NavigationBar
산단에서 제목, Push/Pop 스타일의 내비게이션 구현
- TabBar
화면 하단에서 화면을 교체하는 UI에서 사용.
- SearchBar
검색 기능
모든 시스템 뷰는 UIKit을 통해 제공된다.
UIKit 내의 UIView는 크기, 투명도 등의 모든 시스템 뷰의 공통적인 기능들이 구현되어 있다.
UIControl은 Control, TextField의 공통기능이 구현되어있다.
UIScrollView는 스크롤 기능을 가진 Table, Text, Collection 등의 공통기능이 구현되어있다.
View의 역할과 좌표체계
화면에 표시되는 모든 UI를 View(뷰)라고 부른다.
뷰는 자신의 영역에서 데이터를 표시하고 터치 이벤트를 처리한다.
이러한 기능들은 UIKit에 구현되어있다.
UIView는 모든 뷰가 공통으로 상속받으며,
UI 구현에 필요한 필수 기능들이 구현되어있다.
따라서 모든 뷰에 공통으로 적용된다.
- 뷰는 컨텐츠를 화면에 출력하거나 앱이 처리하는 데이터, 이벤트 처리 UI를 출력한다.
모든 뷰는 고유의 Frame을 가지며, 모든 작업은 Frame 내에서 이루어진다.
단, 컨텐츠의 경우 Frame 외부에 표시하는 것이 가능하다. - UIView가 제공하는 방식으로 화면에 표시하는 방법을 지정한다.
화면에 무언가를 그리는 것은 꽤나 고비용의 작업으로 Bitmap Cache를 사용해 재사용하게 된다.
이러한 방식을 On-demand Drawing Model이라고 한다. - Content Mode는 이 Bitmap Cache를 재사용하는 방식을 설정한다.
Scale to fill, Aspect Fit, Aspect Fill 등이 있다. - 터치 이벤트를 처리하거나 Superview로 전달하는 역할을 한다.
터치 이벤트를 처리하는 뷰를 Controller라고 부르고 이에는 Button, Switch, Slider 등이 해당된다.
처리하지 않는 뷰에는 Label, Image 등이 있다. - 터치 이벤트는 탭, 더블택, 프리킹, 패닝, 롱 프레스 등이 있으며, 각각 직접 구현하지 않아도 사용할 수 있다.
- 자신에게 포함된 뷰를 관리한다.
모든 뷰는 자신의 Frame 안에 하나 이상의 뷰를 배치할 수 있다.
단, 하나의 뷰가 동시에 다른 SuperView를 가질 수 없다.
SubView는 배열로 관리되며, 따라서 순서가 존재하고, 이는 화면에 표시되는 순서와 같다.
또한, Class의 상속과 비슷하게 SuperView의 변화에 따라 SubView가 영향을 받을 수 있다.
Coordinate (좌표 체계)
뷰의 크기는 좌상 단부터 시작해 pt(포인트) 단위로 결정된다.
뷰와 윈도우는 각각 고유의 지역 좌표를 가진다.
뷰에 표시되는 컨텐츠의 크기는 뷰의 좌표를 따르고,
뷰의 위치와 크기는 SuperView의 좌표를 따른다.
이때 위치와 크기를 합쳐 Frame이라 하고,
Frame은 CGRect 형태를 가진다.
public struct CGRect {
public var origin: CGPoint
public var size: CGSize
public init()
public init(origin: CGPoint, size: CGSize)
}
CGRect는 origin과 size를 가지고 있고, 이들은 각각 CGPoint와 CGSize의 형태를 가진다.
Frame과 비슷한 개념으로 Board가 존재한다.
Bound는 뷰의 크기만 나타내고, 형태는 동일한 CGRect이다.
View 구성하기
Interface Builder 사용하기
라이브러리를 통해 View를 씬에 추가하고,
Attribute Indicator에서 Background 색을 바꾸면 간단히 끝난다.
코드로 구현하기
Interface Builder를 사용하지 않고 다음과 같이 코드로 직접 구성할 수 있다.
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
씬에 연결된 클래스 파일로 이동했다.
뷰가 생성된 다음 실행되면 되기 때문에 viewDidLoad 메서드 안에서 작성을 시작한다.
View의 위치 지정
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let frame = CGRect(x: 50, y: 50, width: 100, height: 100)
}
}
모든 컨트롤러에는 View라는 이름의 RootView가 존재한다.
따라서 해당 뷰에서의 좌표로 크기와 위치를 정하게 된다.
UIView 생성
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let frame = CGRect(x: 50, y: 50, width: 100, height: 100)
let testView = UIView(frame: frame)
}
}
파라미터로는 앞서 생성한 frame을 전달한다.
SubView 추가
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let frame = CGRect(x: 50, y: 50, width: 100, height: 100)
let testView = UIView(frame: frame)
view.addSubview(testView)
}
}
addSubView 메서드를 사용하고, 파라미터로는 앞서 만든 testView를 전달한다.
배경색 변경
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let frame = CGRect(x: 50, y: 50, width: 100, height: 100)
let testView = UIView(frame: frame)
view.addSubview(testView)
testView.backgroundColor = UIColor.orange
}
}
testView의 Background 속성에 접근해 배경색을 바꾼다.
결과
설정한 좌표인 화면의 좌상 단부터 50, 50 위치에 100*100의 주황색 View가 생성됐다.
Navigation Bar나 다른 인터페이스에 간섭받지 않기 위해선
감안한 frame을 전달하거나 제약조건을 추가해야 한다.
Content Mode
이번 씬은 ImageView와 Label, Button으로 구성한다.
버튼을 누르면 ImageView의 Content Mode를 변경하고, 모드의 이름을 Label에 출력한다.
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var ModeLabel: UILabel!
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func NextModeBtn(_ sender: Any) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
ImageView와 Label은 Outlet으로 연결하고,
동작을 받을 Button은 Action으로 연결한다.
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var ModeLabel: UILabel!
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func NextModeBtn(_ sender: Any) {
}
func labelChange() {
switch imageView.contentMode {
case .scaleToFill:
ModeLabel.text = "Scale to fill"
case .scaleAspectFit:
ModeLabel.text = "Scale aspect fit"
case .scaleAspectFill:
ModeLabel.text = "Scale aspect fill"
case .redraw:
ModeLabel.text = "Redraw"
case .center:
ModeLabel.text = "Center"
case .top:
ModeLabel.text = "Top"
case .bottom:
ModeLabel.text = "Bottom"
case .left:
ModeLabel.text = "Left"
case .right:
ModeLabel.text = "Right"
case .topLeft:
ModeLabel.text = "Top left"
case .topRight:
ModeLabel.text = "Top right"
case .bottomLeft:
ModeLabel.text = "Bottom left"
case .bottomRight:
ModeLabel.text = "Bottom right"
@unknown default:
fatalError()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
이후 ImageView의 모드에 따라 분기해 Label의 내용을 바꾸는 메서드를 작성한다.
public enum ContentMode : Int {
case scaleToFill = 0
case scaleAspectFit = 1 // contents scaled to fit with fixed aspect. remainder is transparent
case scaleAspectFill = 2 // contents scaled to fill with fixed aspect. some portion of content may be clipped.
case redraw = 3 // redraw on bounds change (calls -setNeedsDisplay)
case center = 4 // contents remain same size. positioned adjusted.
case top = 5
case bottom = 6
case left = 7
case right = 8
case topLeft = 9
case topRight = 10
case bottomLeft = 11
case bottomRight = 12
}
ContentMode는 위와 같이 Int의 원시 값을 가지는 열거형으로 구현되어 있어
다음과 같이 ContentMode를 변경하는 코드를 구현할 수 있다.
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var ModeLabel: UILabel!
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func NextModeBtn(_ sender: Any) {
let currentMode = imageView.contentMode.rawValue
let nextMode = UIView.ContentMode(rawValue: currentMode + 1) ?? .scaleAspectFill
imageView.contentMode = nextMode
labelChange()
}
func labelChange() {
switch imageView.contentMode {
case .scaleToFill:
ModeLabel.text = "Scale to fill"
case .scaleAspectFit:
ModeLabel.text = "Scale aspect fit"
case .scaleAspectFill:
ModeLabel.text = "Scale aspect fill"
case .redraw:
ModeLabel.text = "Redraw"
case .center:
ModeLabel.text = "Center"
case .top:
ModeLabel.text = "Top"
case .bottom:
ModeLabel.text = "Bottom"
case .left:
ModeLabel.text = "Left"
case .right:
ModeLabel.text = "Right"
case .topLeft:
ModeLabel.text = "Top left"
case .topRight:
ModeLabel.text = "Top right"
case .bottomLeft:
ModeLabel.text = "Bottom left"
case .bottomRight:
ModeLabel.text = "Bottom right"
@unknown default:
fatalError()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
버튼을 누르면 현재 ImageView의 ContentMode의 원시값을 받아와 1을 더해 다음 모드의 원시 값으로 바꾼 뒤,
ContentMode를 변경하고 앞에서 작성했던 labelChange 메서드를 사용해 Label의 내용을 바꾼다.
//
// ViewCreateViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class ViewCreateViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var ModeLabel: UILabel!
@IBAction func backBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func NextModeBtn(_ sender: Any) {
let currentMode = imageView.contentMode.rawValue
let nextMode = UIView.ContentMode(rawValue: currentMode + 1) ?? .scaleAspectFill
imageView.contentMode = nextMode
labelChange()
}
func labelChange() {
switch imageView.contentMode {
case .scaleToFill:
ModeLabel.text = "Scale to fill"
case .scaleAspectFit:
ModeLabel.text = "Scale aspect fit"
case .scaleAspectFill:
ModeLabel.text = "Scale aspect fill"
case .redraw:
ModeLabel.text = "Redraw"
case .center:
ModeLabel.text = "Center"
case .top:
ModeLabel.text = "Top"
case .bottom:
ModeLabel.text = "Bottom"
case .left:
ModeLabel.text = "Left"
case .right:
ModeLabel.text = "Right"
case .topLeft:
ModeLabel.text = "Top left"
case .topRight:
ModeLabel.text = "Top right"
case .bottomLeft:
ModeLabel.text = "Bottom left"
case .bottomRight:
ModeLabel.text = "Bottom right"
@unknown default:
fatalError()
}
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.layer.borderColor = UIColor.blue.cgColor
imageView.layer.borderWidth = 3
labelChange()
}
}
마지막으로 실습의 시인성을 위해 imageView의 외곽선을 설정해 준 뒤,
뷰가 나타났을 때 현재의 ContentMode를 받아올 수 있도록 한다.
결과
각각의 모드들은 위와 같은 특성을 가지고 있다.
ContantMode 중 Redraw는 일반적인 ContentMode와 차이가 없는 듯 보이지만,
실제 성능 면에서는 차이가 난다.
Cache를 항상 지우고 다시 이미지를 그리기 때문에 안정적이나 그만큼의 처리 시간이 더 필요하다.
Tag
화면엔 View 두 개와 Button이 하나 존재한다.
보통 코드에서 View에 접근하는 경우 Outlet으로 연결해야 하지만,
Tag를 사용하면 Outlet으로 연결하지 않아도 접근할 수 있다.
이러한 방식을 View Tagging이라고 한다.
//
// TagViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class TagViewController: UIViewController {
@IBAction func changeBtn(_ sender: Any) {
if let target = view.viewWithTag(0){
target.backgroundColor = .black
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
Outlet의 이름으로 접근하는 것이 아닌 view에 존재하는 태그에 접근해 backgroundColor를 바꾸도록 구현했다.
이때의 view는
씬의 최상단의 view(root view)이다.
tag가 0으로 설정되어 있는 view에 접근하여 backgroundColor를 검은색으로 바꾼다.
결과
view에 기본적으로 적용되는 태그 값은 0이다.
해당 규칙은 rootView를 포함한 모든 view에 적용되며,
따라서 코드에서 접근하는 0번 태그는 화면에 존재하는 모든 UI들에 해당되고,
최상단에 존재하는 rootView에 접근한 뒤 backgroundColor를 변경한다.
이번엔 해당 view의 tag를 1로 바꾸고, 코드에서도 1로 접근한다.
이번엔 의도한 대로 해당 view의 색이 바뀌는 것을 확인할 수 있다.
따라서 Tag는 유일해야 하며,
화면의 View가 늘어날 때마다 관리해야 하는 Tag의 수도 늘어나 관리가 힘들어진다.
따라서 보통은 Outlet으로 연결하는 경우가 많다.
Interaction
이번 씬에는 하나의 uiview와 두 개의 Switch가 존재한다.
uiView는 터치 입력을 받고, 이를 표시할 수 있도록 별도의 클래스를 상속받도록 한다.
//
// TouchView.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class TouchView: UIView {
let noteImage = UIImage(systemName: "music.note")?.withTintColor(.systemRed)
var points = [CGPoint]()
override func draw(_ rect: CGRect) {
super.draw(rect)
for pt in points {
noteImage?.draw(in: CGRect(x: pt.x, y: pt.y, width: 64, height: 64))
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let pt = t.location(in: self)
points.append(pt.applying(CGAffineTransform(translationX: -32, y: -32)))
}
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
points.removeAll()
setNeedsDisplay()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
points.removeAll()
setNeedsDisplay()
}
}
해당 클래스는 좌표값을 받아 해당 좌표에 특정 이미지를 표시하게 된다.
//
// InteractionViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class InteractionViewController: UIViewController {
@IBOutlet weak var interactionView: UIView!
@IBOutlet weak var On: UISwitch!
@IBOutlet weak var multi: TouchInteraction!
@IBAction func onBtn(_ sender: Any) {
}
@IBAction func multiBtn(_ sender: Any) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
switch는 각각 outlet과 action 모두 연결하고, view는 outlet으로 연결한다.
//
// InteractionViewController.swift
// newProject
//
// Created by Martin.Q on 2021/08/05.
//
import UIKit
class InteractionViewController: UIViewController {
@IBOutlet weak var interactionView: TouchInteraction!
@IBOutlet weak var On: UISwitch!
@IBOutlet weak var multi: UISwitch!
@IBAction func onBtn(_ sender: UISwitch) {
interactionView.isUserInteractionEnabled = sender.isOn
}
@IBAction func multiBtn(_ sender: UISwitch) {
interactionView.isMultipleTouchEnabled = sender.isOn
}
override func viewDidLoad() {
super.viewDidLoad()
On.isOn = interactionView.isUserInteractionEnabled
multi.isOn = interactionView.isMultipleTouchEnabled
}
}
outlet으로 연결된 switch들의 상태를 확인해 설정값을 변경한다.
이때 switch의 상태를 전달하기 위해 sender를 Any가 아닌 UIswitch로 변경한다.
결과
isUserInteractionEnabled은 User Interaction Enabled를,
isMultipleTouchEnabled는 Multi Touch를 활성화한다.
Alpha
알파는 0.0에서 1.0 사이의 값을 가지는 view의 투명도에 관여하는 속성이다.
해당 씬의 두 개의 UIView는 붉은색의 SuperView와 초록색의 SubView 관계를 가지고 있다.
우선 SubView인 초록색 UIView의 알파를 0.5로 변경하면 위와 같이 해당 뷰의 투명도만 변경되는 것을 알 수 있다.
하지만 SuperView인 붉은색 UIView의 알파를 0으로 변경하면
해당 뷰뿐만이 아닌 자신에게 속한 초록색 SubView까지 함께 투명해지는 것을 확인할 수 있다.
이와 같이 SubView의 Alpha는 SuperView에 영향을 주지 않지만,
SuperView의 Alpha는 SubView에 영향을 준다.
BackgroundColor
BackgroundColor는 해당 뷰의 배경색을 바꾼다.
AttributeInspector의 해당 속성으로,
코드에서 변경할 때는 UIClass를 사용한다.
이는 이후에 언급한다.
Hidden
Hidden은 해당 뷰를 숨기고, 이벤트 처리도 금지하도록 성정하는 속성이다.
해당 옵션을 사용한 상태에선 화면에 해당 뷰가 나타나지 않는다.
Clip to bound
뷰의 표시 영역을 Frame 내부로 제한한다.
예를 들어 해당 속성을 이전에 사용했던 ContentMode에서 사용해 보면
결과
뷰의 영역 밖으로 튀어나가던 사진이 영역 내에서만 제한적으로 출력되는 걸 확인할 수 있다.
Opaque, Clear Graphics Context
두 속성은 앱의 그리기 성능에 영향을 미친다.
이전 Alpha의 경우를 떠올려 보자.
SubView는 Alpha가 0.5로, 투명한 상태에서 뒤에 존재하는 SuperView의 색이 비쳐 보인다.
이러한 표현은 두 뷰의 Alpha값을 비교해 합성하는 비용이 생기게 되는데,
Opeque 속성은 이러한 부분에 관여하게 된다.
해당 계산이 필요한 Alpha 1.0 미만의 상태에서 사용하고,
1.0이라면 이러한 연산 과정이 필요 없으므로, 해제해서 자원을 아낄 수 있다.
Clear Graphics Context를 사용하면 뷰를 그리기 전 버퍼를 초기화하게 된다.
이러한 과정을 생략하면 성능에는 긍정적으로 작용할 수 있으나 초기화 과정이 생략되므로,
버퍼를 재사용할 때 이전의 뷰가 나타나는 경우가 생길 수 있다.
'학습 노트 > iOS (2021)' 카테고리의 다른 글
036 ~ 042. Image and Color (0) | 2021.08.19 |
---|---|
026 ~ 035. Activity Indicator, Progress View, Stack View and Alert Controller (0) | 2021.08.16 |
020 ~ 025. Slider, Segment Control, Switch and Stepper (0) | 2021.08.11 |
014 ~ 019. Button, Picker and Page Control (0) | 2021.08.11 |
001 ~ 006. Interface Builder, Outlet and Action, Delegate Pattern (0) | 2021.08.04 |