TableView
에서 indexPath
를 획득하는 방법os_log(_:log:_:)
어제 보낸 PR의 피드백을 정말 빠르게도 오늘 받을 수 있었어요. 코드의 Indentation, UILabel
과 UITextView
간 선택, indexPath
의 획득 방법 등 여러 가지에 대한 피드백을 접수했습니다. 피드백을 접수하고 관련 내용을 학습하여 어떻게 새로운 결과물에 반영하였는지 작성하겠습니다.
결론
스타일은 결국 취향이다. 팀에서 정하는 규칙이 있다면 따르되 취향과 장단점에 따라 본인이 취사 선택하자.
팀원과 프로젝트를 진행하게 되면 각자가 지켜야할 팀의 규율인 그라운드 룰을 수립하고 지키는 것이 굉장히 중요합니다. 이번 주에는 개인 프로젝트를 진행하여 기존부터 적용하고 싶었던 내용들을 자유롭게 시도해볼 수 있었는데요, 그 중 제가 평소부터 관심을 가지고 있었던 것이 들여쓰기와 라인 래핑 (Line-Wrapping)이었습니다.
순전히 개인의 취향이라고는 하지만, 저는 언어 사용자들이 보편적으로 사용하고 있는 스타일을 따르기를 원했습니다. 그래서 각종 Swift 스타일 가이드를 찾아본 결과 아래와 같은 스타일 가이드들을 찾을 수 있었습니다.
먼저 들여쓰기부터 살펴보시겠습니다. Xcode에서는 기본적으로 들여쓰기가 4 칸으로 설정되어 있죠.
세 스타일 모두 들여쓰기 시 칸 수는 2 칸, 공백을 추가하는 방법은 스페이스로 사용하자 약속하고 있습니다. 코드 길이가 길어 화면을 벗어나게 되어 시각적으로 줄바꿈이 되는 경우와 코드 길이가 길어 실제로 리턴 (엔터) 입력에 의한 줄바꿈 (line wrapping)을 삽입할 때도 현재 있던 위치에서 + 2 칸만큼 공백을 추가하도록 주문하고 있습니다.
Source: Google Swift Style Guide
Source: Raywenderlich Swift Style Guide
Source: StyleShare Swift Style Guide
이미지를 통해 모습을 비교해볼까요?
Indent width: 4 spaces
Indent width: 2 spaces
큰 차이가 나지 않을 수 있지만 아래와 같은 장단점이 있을 수 있겠네요.
++ 야곰의 의견 - 습관은 무서우므로 들여쓰기를 많이 하지 않도록 4 칸을 사용하는 것도 좋다.
혹시 새로운 메서드를 작성하거나 메서드를 호출할 때 무심코 라인 길이가 굉장히 길어진 경험이 있으신가요? 저는 아래 이미지와 같은 경험이 있습니다.
에..? 19 번 라인이 한 줄이라구..?
57 번 라인의 상태가..?
이에 대한 통상적인 스타일에 대해서도 각종 스타일 가이드에서 다루고 있습니다. Line-Wrapping
이라는 이름이죠.
위 이미지와 같이 아주 긴 메서드를 정의하고자 할 때는 어떤 방식으로 라인 래핑을 적용하면 좋을까요? 아래 이미지들을 보시면, 주황색 블럭 내부의 요소들은 라인 래핑을 적용하지 않고, 파란색 블럭 내부의 요소들은 판단에 따라 라인 래핑을 적용하도록 하고 있습니다.
적용한 모습을 보면 아래와 같이 되는 것이죠.
이와 마찬가지로 여러가지 요소들의 라인 래핑 가이드를 제공하고 있습니다.
그럼 스타일 가이드에 따라 위에서 제시한 내용들에 따라 라인 래핑을 적용해 보겠습니다.
어떠신가요? 매개변수 하나당 하나의 라인을 차지하니 가독성이 개선되었다고 느끼시나요? 앞으로도 가독성 높은 코드 작성을 위해 노력해보자구요~!
TableView
에서 상세 뷰로 이동하기 위해 indexPath
를 획득하는 방법부제: prepare(for:sender:)
의 sender
는 무엇일까?
TableView
에서 Cell
을 선택하면 상세 페이지로 이동하게끔 구성해야 하는 경우가 있습니다. 그럼 선택된 Cell
이 무엇인지 알기 위해 indexPath.row
값을 찾아야 하는데요, 이번에는 해당 상황에서 어떻게 indexPath
를 얻을 수 있는지 알아보겠습니다.
이전 포스팅에서 다음 화면으로 정보를 넘겨주는데 UIViewController
의 prepare(for:sender:)
메서드를 사용할 수 있다는 것을 알아봤습니다. 동일하게 해당 메서드를 활용해볼게요.
부제: prepare(for:sender:)
의 sender
는 무엇일까?
TableView
에서 Cell
을 선택하면 상세 페이지로 이동하게끔 구성해야 하는 경우가 있습니다. 그럼 선택된 Cell
이 무엇인지 알기 위해 indexPath.row
값을 찾아야 하는데요, 이번에는 해당 상황에서 어떻게 indexPath
를 얻을 수 있는지 알아보겠습니다.
이전 포스팅에서 다음 화면으로 정보를 넘겨주는데 UIViewController
의 prepare(for:sender:)
메서드를 사용할 수 있다는 것을 알아봤습니다. 동일하게 해당 메서드를 활용해볼게요.
tableView.indexPathForSelectedRow
프로퍼티를 사용한다.왜 TableView
가 아니라 소문자로 시작하는 tableView
일까요? 그 이유는 @IBOutlet
키워드와 함께 생성한 UITableView
의 프로퍼티를 나타내기 때문입니다. 그래서 프로퍼티를 생성하실 때 설정하신 이름으로 적어주시면 됩니다. 그러면 prepare(for:sender:)
메서드를 사용할 때 아래와 같이 indexPath
를 얻을 수 있죠.
// MARK: - View controller: segue
extension ArtworksTableViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let indexPath = tableView.indexPathForSelectedRow
if segue.identifier == "showDetail" {
let followingViewController = segue.destination as? ArtworkDetailViewController
guard let rowOfIndexPath: Int = indexPath?.row else {
os_log(.fault, log: .ui, "indexPath가 nil입니다.")
return
}
followingViewController?.artwork = artworks[rowOfIndexPath]
}
}
}
그럼 두 번째 방법을 살펴볼까요?
prepare(for:sender:)
메서드의 sender
를 활용한다.앞서 말씀드린 prepare(for:sender:)
메서드에서 sender
는 무엇을 의미할까요? 앞서 작성한 코드에서 LLDB
를 통해 살펴본 sender
의 정체를 이미지로 가져왔습니다!
결과적으로 sender
는 해당 메서드를 실행(이 메서드의 경우 segue
)시키는 주체를 나타내는 것으로 확인됩니다. 기본적으로 매개변수 타입 지정에 의해 Any?
타입이며 이 경우에는 Any?
속에 UITableViewCell
타입이네요. Any? -> UITableViewCell
로 다운캐스팅하면 indexPath
를 얻을 수 있으니 아래와 같이 코드를 작성할 수 있겠습니다.
// MARK: - View controller: segue
extension ArtworksTableViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let indexPath = tableView.indexPath(for: sender as! UITableViewCell) else {
os_log(.fault, log: .ui, OSLogMessage.indexPathIsNil)
return
}
if segue.identifier == Identifier.Segue.artworkDetail {
let followingViewController = segue.destination as? ArtworkDetailViewController
followingViewController?.artwork = artworks[indexPath.row]
}
}
}
날 것
에 의미를 부여해보자!: NameSpace코드를 작성하다보면 날 것의 숫자나 텍스트를 적용해야 하는 경우가 있습니다. 예를 들어 JSON 파일로부터 데이터를 불러올 때, 파일의 이름을 작성한다거나, UI 요소에 적용될 접미 접두사 같은 요소들이 있겠죠.
이와 같은 날 것의 코드는 최초 작성자를 포함하여 동료들까지 실수를 일으킬 수 있는 요소가 될 수 있기에 별도의 장소에 함께 작성해두었다가 불러서 사용하는 것이 더 좋은 선택일 수 있습니다. 날 것의 코드에 의미를 부여해줄 수 있기도 하구요.
으잉? "exposition_universelle_1900"이 뭐람.. 파일 이름인가? 파일 이름 바뀌면 다른 곳에서도 다 바꿔야겠네.. 애초에 파일 이름이 맞는지도 잘 모르겠어.
충분히 이런 생각을 할 수 있다고 생각합니다. 이를 미연에 방지하려면 이러한 날 것의 코드를 모아서 관리하는 Name space
를 만들 것을 고려해볼 수 있습니다. 아래 코드를 보실까요?
enum ExpoData {
static let expoIntroduction: String = "exposition_universelle_1900"
static let artworks: String = "items"
static let posterImage: String = "poster"
}
Expo
라는 것이 프로젝트임을 알고있다면, 이 열거 타입은 데이터와 관련된 타입이라는 것을 알 수 있겠네요. 소개와 관련된 자료는 expoIntroduction
이, 미술품과 관련된 자료는 artworks
가 가지고 있는 것도 파악이 가능할 것입니다. 그럼 이미지로 보여드렸던 내용도 아래와 같이 적용할 수 있겠죠.
파일 이름이 변경돼도 이 코드에서 변경할 점은 없겠네요. ExpoData
라는 Name space
에서 변경해주면 다른 파일에서 적용한 내용들도 함께 변경할 수 있으니까요.
마찬가지로 UI 요소에 적용되는 날 것의 텍스트도 이러한 방식으로 관리할 수 있습니다. JSON 데이터에서 48130300
이라는 숫자만 제공하는 상황에서 아래 이미지와 같이 방문객:
, 명
에 해당하는 내용을 접두, 접미어로 붙여주어야할 때 아래와 같이 적용하시면 됩니다.
어떤가요? 저는 날 것의 텍스트가 코드에서 살아 숨쉬는 것 보다 훨씬 좋다고 생각합니다. 텍스트 뿐만 아니라 정수나 소수 모든 타입을 이러한 형식으로 선언하여 원하는 때마다 반복적으로 꺼내 쓸 수 있습니다!
아래가 궁금하시다면 계속해서 읽어주세요~!
사실 Name space
를 정의하는데 열거 타입을 꼭 사용하여야 하거나 타입 프로퍼티를 활용하지 않아도 됩니다. 구조체를 이용하거나 열거 타입의 case - rawValue
를 활용해도 되죠. 이를 활용하면 아래와 같이 작성이 가능합니다.
// 구조체로 Name space를 작성하는 경우
struct ExpoData {
static let expoIntroduction: String = "exposition_universelle_1900"
static let artworks: String = "items"
static let posterImage: String = "poster"
}
// 열거 타입의 case - rawValue 형식으로 작성하는 경우
enum ExpoData: String {
case expoIntroduction = "exposition_universelle_1900"
case artworks = "items"
case posterImage = "poster"
}
실제로 위와 같은 형식으로 사용하기도 하는데요, 저는 개인적인 이유로 case
가 없이 타입 프로퍼티만으로 작성된 열거 타입을 활용합니다. 그 이유는 의도치 않은 인스턴스를 생성하지 않을 수 있기 때문이에요.
예를 들어, 위 예시와 같이 구조체로 Name space
를 작성한 경우 ExpoData()
를 통한 인스턴스 생성이 가능합니다. 아무 기능이 없더라도 의도치 않은 것임에는 틀림없죠. 이는 아래와 같이 이니셜라이저에 접근제한을 걸어둠으로써 인스턴스 생성을 방지할 수 있습니다.
// 구조체로 Name space를 작성하는 경우
struct ExpoData {
static let expoIntroduction: String = "exposition_universelle_1900"
static let artworks: String = "items"
static let posterImage: String = "poster"
private init() { }
}
저는 이렇게 이니셜라이저에 접근제한을 설정하는 것 또한 case
없는 열거 타입에 비해 불필요한 코드가 추가되는 것이라 생각해서 선호하지 않습니다.
case
- rawValue
로 정의된 경우와 비교두 번째 예시를 다시 가져와 보겠습니다.
// 열거 타입의 case - rawValue 형식으로 작성하는 경우
enum ExpoData: String {
case expoIntroduction = "exposition_universelle_1900"
case artworks = "items"
case posterImage = "poster"
}
제가 위와 같이 사용하지 않는 이유는 아래와 같습니다.
.rawValue
를 추가해야 한다는 번거로움이 있습니다. ExpoData.artworks.rawValue
처럼요. 저는 이렇듯 불필요하게 추가되는 요소를 선호하지 않습니다. case
가 존재하는 열거 타입의 경우 rawValue
를 활용해서 이니셜라이징할 수 있으므로 구조체와 마찬가지로 의도치 않은 인스턴스를 생성할 수 있다는 단점이 존재합니다.rawValue
가 하나의 타입 (위 예시의 경우 String
)으로 고정되므로 관련 있는 여러 타입을 하나의 Name Space
에서 관리할 수 없습니다.하지만 이 모든 것은 제 선호일 뿐이고 사용하시는 분들이 자유롭게 취사선택하시면 되겠습니다.
스토리보드에서 UI 요소를 화면에 올리고 코드와 연결하는 작업을 할 때 @IBOutlet
속성의 프로퍼티를 만들죠. 여러분들은 이들의 이름을 어떻게 지으시나요? 저는 코드 리뷰를 받으며 UI 요소의 이름을 짓는 것에 대해 다시 한 번 생각해 보았어요.
여느 때처럼 즐겁게 코드를 작성하던 찰나, 진행하던 프로젝트의 PR에 대한 피드백 (코드 리뷰)을 받았습니다.
이미지에서 피드백해주신 코드는 옵셔널 바인딩 결과에 따라 에러를 debugPrint
해주는 부분이었는데요, 과연 링크를 남겨주신 부분이 무엇일까 클릭해보았더니 Zedd님께서 os_log를 주제로 작성하신 글이었습니다. 요지는 Unified logging System (통합 로깅 시스템)에 메시지를 로깅할 수 있는 방식이라는 것이네요. 제 나름대로 내용을 공부해 가볼게요.
OSLog (프레임워크)
과거의 데이터를 읽기 위한 통합 로깅 시스템.
OSLog 프레임워크는 사용자 (프로그래머)가 로그를 읽을 수 있게 해준다. 통합 로깅 시스템을 사용하면 Instrument 및 Console과 같은 Apple 툴과 함께 사용할 사용자 지정 디버깅 및 분석 툴을 구축할 수 있게 해준다.
오케이, 링크된 Logging을 살펴봅시다.
Logging
통합 로깅 시스템을 사용하여 디버깅 및 성능 분석을 위해 앱에서 원격 측정 (telemetry)을 캡처한다.
아직 통합 로깅 시스템에 대한 이해는 하지 못했지만 제가 기존에 에러가 일어나면 print
를 해서 콘솔창으로 확인하던 방식과 달리 로그를 남길 수 있는 방법인가 보네요. 계속 읽다보니 문서에 중요한 내용이 있었습니다.
통합 로깅 시스템은 시스템의 모든 수준에서 원격 측정을 캡처할 수 있는 포괄적이고 성능이 뛰어난 API를 제공합니다. 이 시스템은 데이터를 텍스트 기반 로그 파일에 쓰지 않고 메모리 및 디스크에 로그 데이터를 중앙 집중식으로 저장합니다. 콘솔 앱, 로그 명령줄 도구 또는 Xcode 디버그 콘솔을 사용하여 로그 메시지를 보거나 OSLog 프레임워크를 사용하여 로그 메시지에 프로그래밍 방식으로 액세스할 수도 있습니다.
그렇군요. 성능이 좋고 모든 수준에서 사용할 수 있다. 모든 수준에 대해 궁금하신 분은 이 문서를 읽어보세요. 아래와 같은 표를 찾으실 수 있으실 것입니다.
수준
에 대해 간단하게 보자면 아래와 같습니다.
또 한가지 중요한 점은 통합 로깅 시스템이 iOS 10, macOS 10.12, tvOS 10.0, watchOS 3.0 이후 버전부터 지원한다는 점입니다.
준비해둔 에러 상황에서 어떻게 작동하는지 확인해보겠습니다. mac에 기본 탑재된 console
앱으로 확인해볼게요.
os_log(_:)
import OSLog
os_log("artWork is nil.")
os_log(_:log:_:)
os_log(.error, log: .default, "artwork is Nil.")
os_log(.fault, log: .default, "artwork is Nil.")
물론 콘솔에서도 아래와 같이 로깅된 내용을 확인할 수 있습니다.
아래와 같이 OSLog
클래스의 인스턴스를 OSLog
프레임워크에 타입 프로퍼티로 만들어 확장해주시면 원하시는 subsystem
과 카테고리로 메시지를 로깅하실 수 있습니다.
import OSLog
extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!
static let ui = OSLog(subsystem: subsystem, category: "UI")
static let data = OSLog(subsystem: subsystem, category: "Data")
}
// 메시지 로깅이 필요한 위치에서 아래 메서드를 호출
os_log(.fault, log: .data, OSLogMessage.artworkIsNil)
메시지 타입으로 StaticString
을 요구하여 문자열 보간법과 같은 방법을 직접적으로 적용할 수는 없지만 아래와 같은 방법으로 우회적으로 적용할 수 있습니다. 참고 자료
// 아래는 메시지가 `StaticString` 타입이 아니므로 에러가 발생합니다.
os_log(.info, log: .network, "New used logged in with name \(user.name)") // 에러 발생!
// 메시지를 미리 선언하여 아래와 같이 우회적으로 사용할 수 있습니다.
let message = "New used logged in with name \(user.name)"
os_log(.info, log: .network, "%@", message)
읽어주셔서 감사합니다!