오토 레이아웃을 잘 다루려면 어떻게 해야할까?
06 Mar 2020 | iOS개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
우리가 일반적으로 아는 영어뜻처럼 Optional이라는 것은 선택적인 이라는 뜻을 가진다. 선택적인 이라는 단어뜻처럼 이는 스위프트에서도 있어도 되고 없어도 된다는 의미를 가진다. 즉, 코딩을 하면서 어떤 변수에 값이 있을수도 있고 없을 수도 있는 경우를 위해서 존재한다. swift 개발을 할때 ? 혹은 !
기호를 자주 봤을텐데, swift에서는 이 기호들을 타입 어노테이션 옆에 붙여줘야 한다.
var test: Int
test = nil
위 간단한 코드를 봐보도록 하자.
반드시 에러가 뜰 것이다 > nil은 Int형 태입에 할당될 수 없다.
swift에서는 기본적으로 변수를 선언할 때 non-optional인 값을 할당해주어야 한다. 즉 어떠한 값을 변수에 반드시 할당해주어야 하는데 위 코드에서는 우리가 변수에 Int형으로 변수를 선언하였음에도 불구하고 test라는 변수에 nil을 할당해주었기 때문에 에러가 뜨게 된것이다.(nil은 어떠한 값이 아니기 때문에 발생하는 에러) 따라서 애초에 Int 타입에 Int 타입이 아닌것을 넣었기 때문에 Int라는 메모리 공간이 초기화되지 못한 것을 의미한다.
그러나 우리가 코딩을 하다보면, 반드시 값이 그때그때 할당되지 않는 경우도 있을 것이다.
즉, 변수안에 값이 확실히 있다는 것을 보장할 수 없을때 Optional을 사용한다.
var test: Int?
test = nil
이제 위에처럼 코드를 변경해보자. 에러가 사라졌을 것이다. 이때 ?가 아닌 !를 사용해도 에러는 사라질 것이다(optional기호를 사용했기 때문)
이 변수 안에 값이 있을수도 없을수도 있다는 것을 명시해줌으로써 optional변수는 초기화하지 않으면 nil로 자동 초기화가 되기 떄문에 위 같은 코드도 정상적으로 동작하는 것을 볼 수 있다.
optional값을 만들때 우리는 아래와같은 상자를 생각해보면 이해하기 쉬워진다.
옵셔널로 변수를 선언했고 그때 ?를 사용했다면 우리는 ‘노크’를 했다고 생각하면 된다.
이때 만약 값을 가지고 있다면 해당하는 값을 반환해주고 값이 없다면 nil을 반환하게 된다. 이때 nil 또한 메모리를 차지하고 있다.
var someValue: Int? = 30
var Value = someValue
이때 위 코드에서 Value의 타입은 무엇일까? 바로 옵셔널이다.
즉, Value는 옵셔널 타입이며 Int데이터형을 가질 수 있는 변수를 의미한다. 이는 Int일수도 있고 Int가 아닐수도 있다.
var someValue: Int? = 30
var Value: Int = someValue
그런데 만약 위처럼 Value에 Int 타입을 지정해줬다고 해보자. 오류가 뜨게 될 것이다.
기본적으로 Int와 Int?는 다른 타입이다.
someValue는 옵셔널타입으로 값을 가질수도 있고 nil을 가질수도 있다. 그런데 Value에는 무조건 Int 타입만을 가질것이라고 선언을 해줬다. 그러니 당연히 Value는 someValue를 받아들이지 못할 것이다. 따라서 Int와 Int? 즉 옵셔널인 것과 옵셔널이 아닌것은 완전히 다른 타입을 의미하는 것을 알아야 한다.
!는 ?와 다르게 강제로 값을 빼내오는 것을 의미한다. 유명한 짤로 망치로 상자를 부수는 것이 떠오를 것이다.
이는 강제 언래핑(Unwrapping)이라고 하고 이 단어가 의미하는 것은 상자안에 값이 있든 없든 무조건 값을 가져오겠다는 것을 의미한다.
깨부순 상자안에는 값이 있을수도 있고 없을 수도 있을 것이다.
var someValue: Int? = 30
var Value: Int = someValue
위에서 본 예제를 다시 한번 봐보도록 하자. 오류가 났던 위 예제를 아래처럼 고쳐보도록 하자
var someValue: Int? = 30
var Value: Int = someValue!
자. 강제로 상자를 부쉈기때문에 에러는 발생하지 않았을 것이다. 그리고 다행히 상자 안에는 30이라는 값이 있었기 때문에 30이라는 숫자또한 값을 가지게 될 것이다. 그렇기 떄문에 옵셔널이 아닌 Int형의 Value라는 변수안에 데이터(30)이 들어갈 수 있게 될 것이다. 상자를 부수고, 값을 꺼내어 그 값을 Value안에 넣어준 코드이니 Value입장에서는 문제가 없을 것이다. (Value는 Int형 데이터만을 받고 있는데 운이좋게 someValue에는 30이라는 Int형의 데이터가 있었기 때문이다)
그런데 만약 상자안에 값이 없다면? 생각해보자
var someValue: Int? = nil // someValue는 옵셔널타입으로 nil이 들어갈 수 있다
var Value: Int = someValue!
컴파일 오류도 일어나지 않을 것이며 build success도 되겠지만 금방 런타임에러가 발생하게 될 것이다.
!를 사용해 값이 존재하지 않는 옵셔널 값에 접근하려 시도하면 런타임 에러가 발생한다.
느낌표를 사용해 강제 언랩핑을 하기 전에는 항상 옵셔널값이 nil이 아님을 확실히 해야한다.
즉, nil이 아니라는 것이 확실하지도 않은 상태에서 !를 남용하면 에러가 날 확률도 높아질 것이다. 그리고 !도 옵셔널이기 때문에 초기화할 때 갑을 요구하지 ㅇ낳는다. 초기화해주징낳으면 ?와 마찬가지로 nil값이 들어가게 될 것이다. 잊지말자
그러면 이제 옵셔널 타입의 변수값을 가져오는 방법에 대해 생각해 보자
옵셔널 바인딩은 주로 if let(if var)구문과 같이 사용된다. 즉 먼저 체크해준다 를 생각하면 이해하기 쉽다.
이 값이 nil인지, 값이 있는지 경우에 따라 결과를 달리하고 싶을때 옵셔널 바인딩을 사용해 겁사해주면 된다.
func printName(_name:String) {
print(_name)
}
var myName: String? = nil
if let name = myName {
printName(_name: name)
}
실행해보면 콘솔창에는 아무것도 나오지 않을 것이고(myName은 nil값으로 초기화되어있기 때문) 이는 if let구문이 myName이라는 상자에 노크해보고 값이 있으면 name에 넣어주고 조건문을 실행해 라는 것을 의미한다는 것을 알 수 있을 것이다. 이때 값이 있는 경우에만 값이 바인딩 되기 때문에 콘솔창에는 아무것도 실행되지 않아 비어져있을 것이다.
옵셔널 체이닝은 하위 프로퍼티에 옵셔널값이 있는지 없는지를 연속적으로 확인하면서 중간에 하나라도 nil이 존재함다면 nil이 반환되는 형식을 의미한다.
let roomCount = zehye.residence?.numberOfRoomes
위처럼 진행하는 방식이 옵셔널 체이닝이다. 이때 zehye의 residence가 nil이 아니라면 다음으로 넘어가 residence의 numberOfRoomes를 확인하게 될 것이다. 만약 zehye의 residence가 nil이라면 뒤의 else문을 수행하게 될 것이다. 그런데 이때 residence 뒤에 왜 ?가 붙을까?
그 이유는 residence가 nil을 반환할 수도 있고 아닐수도 있기 때문이다.
즉 하위 프로퍼티에 옵셔널 값이 있는지 연속적으로 확인하면서 중간에 하나라도 nil이 발견된다면 nil을 반환하는 것을 옵셔널체이닝 방식이라고 한다.
그러면 이때 roomCount의 타입은 무엇일까? 당연히 옵셔널 타입일 것이다. 즉 우리가 특별히 지정해주지 않았음에도 불구하고 roomCount의 타입은 옵셔널임을 알 수 있다. 그 이유는 zehye.residence?.numberOfRoomes가 nil을 반환할 수도 있고 아닐수도 있기 때문이다.
그러면 도대체 !를 왜 쓰는 것일까? 일단 우선적으로 !는 최대한 적게 사용하는 것이 좋다. 그러나 유용하게 쓸 수 있는 구석은 있다.
때로는 프로그램의 구조상 옵셔널 값을 먼저 설정한 후 그 값이 항상 있는 것이 명백한 경우가 있다.
즉 변수에 값이 있다는 것을 확신할 수 있는 경우를 의미한다. 그런때에 우리가 옵셔널 바인딩과 체이닝을 사용해 조건문을 사용할 이유가 있을까? 없다고 생각한다! 이런 종류의 옵셔널을 implicitly unwrapped optionals이라고 정의한다. 직역을 하자면 암시적으로 언랩핑된 옵셔널? 이라고 할 수 있을 것이다.
이는 주로 클래스 초기화 과정에서 사용된다고 한다.
class ViewContrller: UIViewController {
@IBOutlet weak var leftButton: UIButton!
}
이렇게 IBOutlet같은 변수는 연결했다는 것을 확실히 할수 있기 때문에 !를 붙힐 수 있는 것이다. 근데 이렇게 IBOutlet을 선언했음에도 이 버튼을 사용할수도 안사용할수도 있을 텐데 그럴때는 ?를 사용하면 된다. 만약 ? 혹은 ! 둘다 붙이지 않는다면 에러가 뜨게 될 것이다 > UIButton타입은 non-optional일 수 없는 프로퍼티라는 의미의 에러일 것이다.
지금까지의 실습에서 나는 무조건 IBOutlet 뒤에는 무조건 !를 썼지만 사실 이런 사용법도 좋은 방법이 아니라고 한다. 무조건적으로 !의 사용은 줄이는 것이 좋다고 한다. 지금에야 확실히 이 버튼을 사용한다는 확신이 있지만 연결이 되지 않은 경우도 생길 수 있기 때문이다. 즉 만약을 대비하여 UIButton?을 사용하는 것이 훨씬 좋은 방법이라고 한다! 앞으로의 코드는 ?를 사용하도록 해야지
즉 정리하면 !는 옵셔널 바인딩, 체이닝으로 매번 값을 추출하기 귀찮거나 로직상으로 nil이 할당되지 않을 것 같다는 확신이 들때 사용하면 될 것같다.
swift는 일반적으로 아래와 같은 언어이다
옵셔널을 통해 swift의 안정성을 제공한다.
그리고 더 나아가 프로그래머들간의 원활한 커뮤니케이션을 위해 사용한다고 생각한다.
코드만으로도 변수들이 어떤 타입을 가질지 명확하게 보여주며 함수의 파라미터 부분만 봐도 여기에 어떠한 값들이 들어올 수 있는지 추측이 가능하기 때문이다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
스위프트 4 버전 이전에는 JSONSerialization을 사용해 JSON 타입의 데이터를 생성했다. 그러나 스위프트 4 버전부터 JSONEncoder / JSONDecoder가 Codable 프로토콜을 지원하기 때문에 JSONEncoder / JSONDecoder와 Codable 프로토콜을 이용 해 손쉽게 JSON 형식으로 인코딩 및 디코딩할 수 있다.
즉, JSONEncoder 및 JSONDecoder를 활용하여 스위프트 타입의 인스턴스를 JSON 데이터로 인코딩, JSON 데이터에서 스위프트 타입의 인스턴스로 디코딩할 수 있다.
Codable 프로토콜을 준수하는 GroceryProduct 구조체의 인스턴스를 JSON 데이터로 인코딩하는 방법
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let data = try encoder.encode(pear)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
// ----- 출력
{
"name" : "Pear",
"points" : 250,
"description" : "A ripe pear."
}
// Tip : encoder.outputFormatting = .prettyPrinted 설정하면 들여쓰기를 통해 가독성이 좋게 출력해줍니다.
JSON 데이터를 Codable 프로토콜을 준수하는 GroceryProduct 구조체의 인스턴스로 디코딩하는 방법
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
/// 스위프트 4 버전부터 """를 통해 여러 줄 문자열을 사용할 수 있습니다.
let json = """
{
"name": "Durian",
"points": 600,
"description": "A fruit with a distinctive scent."
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
do {
let product = try decoder.decode(GroceryProduct.self, from: json)
print(product.name)
} catch {
print(error)
}
// ----- 출력
"Durian"
새 프로젝트를 하나 만들고 기존에 공유받은 json파일을 assets폴더에 import 해준다.
json 예시
{
"name":"하나",
"age":22,
"address_info": {
"country":"대한민국",
"city":"울산"
}
}
해당 코드를 처리해줄 swift File하나를 만들어준다.
그리고 해당 폴더에 아래와 같이 작성해준다.
struct Friend: Codable {
struct Address: Codable {
let country: String
let city: String
}
let name: String
let age: Int
let addressInfo: Address
}
그리고 Main.storyboard에 tableView하나와 tableViewCell 하나를 만들어준다. 해당 테이블뷰에 연결될 outlet을 viewController에 작성해준 뒤 드래그해서 연결해주고, tableViewCell의 style은 Subtitle Identifier: cell로 지정해준다.
그리고 viewController로 넘어가 간단하게 코드를 작성해준다.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
let cellIdentifier: String = "cell"
var friends: [Friend] = []
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.friends.count // friend의 수만큼 보여달라
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier, for: indexPath)
let friend: Friend = self.friends[indexPath.row]
// cell.textLabel?.text = friend.name + "(\(friend.age))"
// cell.detailTextLabel?.text = friend.address_info.city + ", " + friend.address_info.country
cell.textLabel?.text = friend.nameAndAge // 해당 코드를 사용해주기 위해서 Friend.swift 파일을 수정해주어야 한다
cell.detailTextLabel?.text = friend.fullAddress
return cell
}
override func viewDidLoad() {
super.viewDidLoad() // 친구를 가져옴 > 뷰가 로딩된 이후에 해야할일을 적어줌
// Do any additional setup after loading the view.
// assets을 통해 Json 데이터를 가져옴
let jsonDecorder: JSONDecoder = JSONDecoder()
guard let dataAssets: NSDataAsset = NSDataAsset(name: "friends") else {
return //
}
// jsonDecorder를 가지고 데이터 에셋의 데이터를 통해 json 데이터를 통해 friends를 불러옴
do {
self.friends = try jsonDecorder.decode([Friend].self, from: dataAssets.data)
} catch {
print(error.localizedDescription)
}
self.tableView.reloadData()
}
}
viewController의 delegate를 연결해주고 위 코드를 사용하기 위한 Friend.swift 파일 수정
import Foundation
struct Friend: Codable {
struct Address: Codable {
let country: String
let city: String
}
let name: String
let age: Int
let addressInfo: Address
var nameAndAge: String {
return self.name + "(\(self.age))"
}
var fullAddress: String {
return self.addressInfo.city + ", " + self.addressInfo.country
}
enum CodingKeys: String, CodingKey {
case name, age
case addressInfo = "addess_info"
}
}
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
또, 인코더(Encoder)는 부호화를 수행하는 장치나 회로, 컴퓨터 소프트웨어, 알고리즘을 뜻함!
스위프트4 버전에서는 스위프트의 인스턴스를 다른 데이터 형태로 변환하고 그 반대의 역할을 수행하는 방법을 제공한다. 스위프트의 인스턴스를 다른 데이터 형태로 변환할 수 있는 기능을 Encodable 프로토콜로 표현 하였고, 그 반대의 역할을 할 수 있는 기능을 Decodable로 표현 해 두었으며, 둘을 합한 타입을 Codable로 정의 해두었다.
typealias Codable = Decodable & Encodable
Codable은 스위프트 4 버전에서 처음 소개한 프로토콜로 Codable은 다양한 상황에서 사용할 수 있다. 예를 들어 JSON 형식으로 서버와 애플리케이션이 통신한다면 Codable 프로토콜을 이용해 편리하게 인코딩 및 디코딩할 수 있다.
Coordinate 타입과 Landmark 타입의 인스턴스를 다른 데이터 형식으로 변환하고 싶은 경우에 Codable 프로토콜을 준수하도록 하면된다. Codable 타입의 프로퍼티는 모두 Codable 프로토콜을 준수하는 타입이어야 하며, 스위프트의 기본 타입은 대부분 Codable 프로토콜을 준수한다.
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}
struct Landmark: Codable {
var name: String
var foundingYear: Int
var vantagePoints: [Coordinate]
var metadata: [String: String]
var website: URL?
}
자주 사용하게 될 JSON 형태의 데이터로 상호 변환하고자 할 때는 기본적으로 인코딩/디코딩할 JSON 타입의 키와 애플리케이션의 사용자정의 프로퍼티가 일치해야 한다.
만약 JSON의 키 이름을 구조체 프로퍼티의 이름과 다르게 표현하려면 타입 내부에 String 타입의 원시값을 갖는 CodingKeys라는 이름의 열거형을 선언하고 CodingKey 프로토콜을 준수하도록 하면 된다. CodingKeys 열거형 케이스의 이름은 해당 프로퍼티의 이름과 일치해야 하며 프로퍼티의 열거형 케이스의 값으로 매칭할 JSON 타입의 키를 할당하면 된다. 만약, JSON 타입의 키와 프로퍼티 이름이 일치한다면 값을 할당하지 않아도 무방하다!
struct Landmark: Codable {
var name: String
var foundingYear: Int
var location: Coordinate
var vantagePoints: [Coordinate]
enum CodingKeys: String, CodingKey {
case name = "title"
case foundingYear = "founding_date"
case location
case vantagePoints
}
}
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
1.[Editor] > [Embed In] > [Navigation Controller] 선택
2.view controller 추가 생성 후 해당 뷰 컨트롤러에 대한 클래스 파일 생성
3.셀을 클릭하면 SecondViewController로 넘어갈 수 있도록 설정
4.새로 생성한 SecondViewController의 클래스 명을 지정후 해당 뷰 컨트롤러에 레이블 하나 추가
5.셀의 텍스트가 레이블에 넘어갈 수 있도록 레이블에 해당하는 outlet 작성후 드래그 연결
import UIKit
class SecondViewController: UIViewController {
var textToSet: String?
@IBOutlet weak var textLabel: UILabel!
}
6.아래의 코드를 참고하여 세그를 코드로 구현 > viewController에 작성
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
위 코드의 의미 > 스토리보드를 사용한 애플리케이션에서 네비게이션 되기 전에 준비해야될 것이 있을 것이다. segue.destination을 사용해 다음 뷰 컨트롤러를 가져올 수 있고 그곳에 원하는 내용을 보내줄 수 있다.
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
guard let nextViewController: SecondViewController = segue.destination as? SecondViewController else {
return
}
guard let cell: UITableViewCell = sender as? UITableViewCell else {
return // 네이게이션의 흐름을 만드는 sender
}
nextViewController.textToSet = cell.textLabel?.text // 셀에 해당하는 텍스트를 다음화면으로 전달
// nextViewController.textLabel.text = cell.textLabel?.text
}
7.뷰가 나타나기 전에 레이블에 글자가 보이도록 코드 작성 > SecondViewController에 작성
import UIKit
class SecondViewController: UIViewController {
var textToSet: String?
@IBOutlet weak var textLabel: UILabel!
override func viewWillAppear(_ animated: Bool) { // 화면이 보여지기 전에 레이블에 이미 글자가 셋팅이 되어있어야 함
super.viewWillAppear(animated)
self.textLabel.text = self.textToSet
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}