Medium - Creating iOS Apps Without Storyboards

원문 - Corey Davis

Trend 파악을 Medium 기고문 요약 포스팅 - 스토리보드 없이 앱을 만드는 것에 대하여 500x400 Photo by Hal GateWood on Unsplash

많은 iOS 개발관련 기사와 연습 예제는 스토리보드를 사용하여 사용자 인터페이스를 만듭니다. 이것은 꽤 타당합니다 왜냐하면 만들고 디자인을 수정하는 과정이 시각적으로 보이고 학습 속도를 향상 시켜주기 때문입니다. 그러나 당신이 혼자 개발하는 것이 아니라면 이것은 전문적인 팀으로 iOS를 개발하는 환경에서는 완벽하지 않습니다. 당신이 얼굴을 맞대고 스토리보드가 없거나 내용물이 별로 없는 앱에 대하여 얘기할 때는 당황스러울 수 있습니다. 이런 일이 생기기 전에 스토리보드 없이 앱이 어떻게 동작하는지 알아봅시다.

The Case for Using a Programmatic Layout

왜 많은 팀들이 XIBs와 스토리보드를 삭제할까요?

저는 매우 시각적인 요소에 민감하고 처음에는 이런 접근에 반대했습니다 그래야 상대방만큼 저도 이해할 수 있었거든요, 만약 당신이 이러한 환경에서 일해본 적이 없다면 당신은 아마 스토리보드를 통합할 때 발생하는 충돌 문제를 해결하느라 고생하셨을 겁니다. 당신은 아마 스토리보드 수정을 해야하는 동료로부터 특정 작업을 하지 말아달라고 부탁받았을 겁니다. 만약 이런 것을 겪은 적이 없다면 앞으로 그럴겁니다. 물론 스토리보드나 XIBs들을 분할하여 이런 이슈들을 줄일 수는 있습니다. 그러나 그것은 확실하지 않고 일단 당신이 스토리보드를 적게 쓴다면 금방 좋은 점들을 알게 될겁니다. 가장 큰 장점은 스토리보드를 쓰는 것보다 UIKit를 더욱 깊이 이해할 수 있다는 것이죠. 그리고 WWDC2019에서 보여진 SwiftUI처럼 스토리보드는 이제 과거로 보내야 할 때입니다.

그래도 위의 질문으로 돌아가서 merge conflict나 블로킹 이슈들을 제외하고도 많은 팀들이 스토리보드를 사용하지 않는 이유 중 하나는 property 값들을 쉽게 리뷰할 수 없기 때문입니다. 그리고 스토리보드 XML을 통해 만들어진 코드는 에러를 만들어 내기 쉬운 경향이 있어서 다시 코딩을 하는 것이 나을 정도 입니다. 예제를 한번 살펴보죠.

여기에 두개의 텍스트 필드와 하나의 버튼을 가진 단일 뷰 어플리케이션이 있습니다. 버튼은 하나의 텍스트필드에 대하여 제약조건을 가지고 있고 그것은 상수로 20입니다.

500x400

디자이너가 제약값을 40으로 증가하길 요청했습니다. 저는 수정을 했고 PR을 제출했죠, 그리고 당신을 리뷰어로 지정했습니다. 당신은 PR에서 변경 사항을 확인할 수 있을겁니다.

<rect key="frame" x="20" y="383" width="374" height="30"/>
<rect key="frame" x="20" y="333" width="374" height="30"/>
<constraint firstItem="pYg-JC-an0" firstAttribute="top" secondItem="Kfc-mn-gVn"
  secondAttribute="bottom" constant="20" id="r92-HL-ecY">
<rect key="frame" x="20" y="383" width="374" height="30"/>
<rect key="frame" x="20" y="333" width="374" height="30"/>
<constraint firstItem="pYg-JC-an0" firstAttribute="top" secondItem="Kfc-mn-gVn"
 secondAttribute="bottom" constant="40" id="r92-HL-ecY">

제가 여기서 어떤 걸 변경했을까요? 당신은 아마 버튼과 텍스트필드 사이에 제약조건이 바뀌었다는 걸 예상할 수 있을겁니다. 그러나 당신은 pYg-JC-an0값과 Kfc-mn-gVn값이 제대로 된 제어인지 확신할 수 있나요? XML을 찾아가서 위의 두 값을 찾으면 확인할 수는 있겠죠, 그러나 컨트롤이 12개가 넘는 복잡한 뷰에서 그 모든 걸 제가 수정했다고 하면 어떨까요? 당신은 정말 모든 걸 추적하기 위해 시간을 쓸건가요? 만약 뷰컨트롤러에 코드가 바로 있고 모든 뷰 로직을 담고 있는게 훨씬 편하지 않을까요?

PR의 리뷰를 쉽게하는 것 뿐만 아니라 관련된 버그 또한 훨씬 다루기 쉬워집니다. 모든 레이아웃에 관련된 설정들이 Xcode inspector에 숨겨져 있는게 아니라 코드로서 당신 앞에 있는 것입니다. 자 그러면 스토리보드를 어떻게 치워버리는 지 알아봅시다.

Getting Started

프로그래머틱 레이아웃을 배우기 위해 Xcode에서 새로운 프로젝트를 만들어 봅시다.

단일 뷰 어플리케이션을 만드세요

500x400

스토리보드빠이라고 이름짓고 적당한 곳에 저장합시다.

500x400

Removing the Storyboard

생성된 프로젝트를 Xcode로 열어서 Main.storyboard를 선택하고 지워버립시다. 확인 대화상자에서 Move to trash를 선택합시다.

500x400

스토리보드 빠이!

프로젝트 네비게이터에서 프로젝트를 선택합니다. 그리고 Deployment 영역에서 Main interface 필드의 Main을 삭제하고 저장합시다.

500x400

프로젝트를 빌드하고 실행을 시키면 성공적으로 앱을 빌드하고 잘 실행될겁니다. 그러나 까만 화면만 보일겁니다. 이것은 지금 당신 앱에 윈도우가 하나도 없기 때문입니다. 그리고 윈도우가 있더라도 ViewController.swift가 루트뷰의 뷰 컨트롤로 설정되어 있지 않아서 그렇습니다. 이것이 당신이 알아차리지 못했던 스토리보드가 자동적으로 관리해주던 것들입니다. 이것이 긍정적으로 들릴 수도 있지만 UIKit로 당신이 작업하면서 몰랐던 측면이 많았다는 것을 뜻하기도 합니다.

이 상황을 해결하기 위해선 아주 조금만 코딩을 하면 됩니다. AppDelegate.swift 파일을 여세요. 당신은 UIWindow 타입의 window 프로퍼티와 다른 비어있는 함수들이 보일겁니다. 실제로 코드가 있는 유일한 메소드는 application(_:didFinishLaunchingWithOptions:)이고 true값을 반환하는 구문이 있습니다. 이 메소드를 다음의 코드로 수정해봅시다.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
   //Create a window that is the same size as the screen
   window = UIWindow(frame: UIScreen.main.bounds)
   // Create a view controller
   let viewController = ViewController()
   // Assign the view controller as `window`'s root view controller
   window?.rootViewController = viewController
   // Show the window
   window?.makeKeyAndVisible()

   return true
 }

application 메소드는 어플리케이션이 막 시작할 때 실행되는 메소드이며 앱의 윈도우를 초기화하고 그 윈도우에 대한 루트 뷰컨트롤러를 만들기에 가장 적절한 타이밍입니다. 위의 코드를 한 줄씩 살펴봅시다.

//Create a window that is the same size as the screen
window = UIWindow(frame: UIScreen.main.bounds)

첫 단께는 윈도우를 만드는 것입니다. UIwindow 메소드를 이용해서 윈도우를 만들고 그것의 크기를 디바이스의 화면 크기로 설정합니다.

// Create a view controller
let viewController = ViewController()
// Assign the view controller as `window`'s root view controller
window?.rootViewController = viewController

그 다음 우리는 새로운 뷰 컨트롤러를 초기화합니다. 그리고 나서 뷰 컨트롤러를 윈도우의 루트 뷰 컨트롤러 프로퍼티로 배정합니다. 루트 뷰 컨트롤러는 윈도우의 내용물을 담기위한 간단한 컨테이너 입니다.

// Show the window
window?.makeKeyAndVisible()

마지막으로 윈도우를 보이게 합니다. 이 메소드는 해당 화면이 보여지게 만드는 쉽고 편리한 메소드 입니다.

빌드를 하고 실행을 시키면.. 당신은 여전히 검정색 화면만 보일 것입니다. 이것은 별로 인상적이지 않죠, 코드가 동작하기는 한걸까요?

ViewController.swift를 열고 viewDidload()메소드의 super.viewDidLoad() 다음에 다음 코드를 추가합시다.

view.backgroundColor = .white

다시 빌드를 하고 실행을 시켜보면 하얀색 화면을 볼 수 있을 것입니다. 축하드립니다. 드디어 스토리보드 없이 앱을 만드셨네요! 그리고 그것은 오직 네줄짜리 코드였죠

우리가 했던 것을 간략히 요약하면 우리는 스토리보드를 없애고 스토리보드를 찾지 않도록 프로젝트를 수정하고 앱의 메인 윈도우를 만들고 해당 윈도우의 루트 뷰 컨트롤러를 만들었습니다. 꽤나 간단하죠, 그러나 드래그 앤 드롭으로 IBOutlet과 IBAction들을 연결하던 컨트롤은 어떻게 해야할까요? 이걸 바로 다음에 알아보겠습니다.

Adding UI Elements

자 그럼 UI 요소들을 어떻게 추가할 수 있는지 알아봅시다. 스토리보드에서는 우리는 텍스트 필드나 버튼에 드래그를 통해서 outlet과 action을 만들고 코드를 작성해 상호작용을 할 수 있었습니다. 그러나 스토리보드 없이는 어떻게 해야할까요? 단일 컨트롤을 이용해서 쉽게 알아봅시다.

ViewController.swift 파일을 열어서 ViewDidLoad() 함수 위에 다음 코드를 추가해 줍시다.

var loginButton: UIButton!

그리고 초기화와 버튼 설정을 위해 viewDidLoad() 메소드에 다음 코드를 입력합시다.

loginButton = UIButton(type: .system)
loginButton.setTitle("Login", for: .normal)
loginButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(loginButton)

한 줄씩 코드를 살펴봅시다.

loginButton = UIButton(type: .system)

이 코드는 Identity Inspector에서 클래스 값과 Attribute Inspector에서 타입 값을 설정하는 것과 동일합니다. 500x400

500x400

500x400

loginButton.setTitle("Login", for: .normal)

이 코드는 Attribute Inspector의 값을 설정하는 것과 똑같습니다.

loginButton.translatesAutoresizingMaskIntoConstraints = false

자 이제 점점 재밌어 질겁니다, 왜냐하면 당신이 한번도 본 적이 없는 것이 나오거든요. 이것은 오토레이아웃을 쓰겠다는 것입니다. 우리가 하고 있던 것과 관련없어 보이고 이것을 명시적으로 설정하는 것이 이상할 수도 있습니다. 그러나 이것이 실제로 스토리보드에서 뷰나 컨트롤을 추가할 때마다 수행되었던 것입니다. 당신은 아마 몰랐겠지만요. 이것은 버튼에 대한 스토리보드 xml 코드입니다.

<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES"
        contentHorizontalAlignment="center" contentVerticalAlignment="center"
        buttonType="roundedRect" lineBreakMode="middleTruncation"
        translatesAutoresizingMaskIntoConstraints="NO" id="DLu-Zl-IKU">

끝부분에 translatesAutoresizingMaskIntoConstraints=”NO”부분이 보일겁니다. 그래요 당신이 뷰나 컨트롤을 추가할 때마다 이렇게 한다면 지루하겠죠, 그러나 시간이 지날수록 자동적으로 될겁니다.

view.addSubview(loginButton)

당신이 스토리보드를 통해 드래그를 하는 것은 부모 뷰의 서브뷰가 되는 것입니다. 당신은 그것에 대해 전혀 생각하지 않았겠지만요 우리는 이것을 명시적으로 선언해야 합니다.

이제 버튼의 위치를 처리해봅시다. 지금은 그냥 중앙에 위치하죠, viewDidLoad() 뒤에 다음 함수를 추가합시다.

func constraintsInit() {
  NSLayoutConstraint.activate([
    loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
  ])
}

이 코드는 로그인버튼의 x,y 앵커를 뷰의 중앙 x,y 앵커로 할당하는 것입니다. 당신이 앵커와 친하지 않다면 관련 예제를 찾아보길 강력히 추천드립니다. 이해하기 쉽고 읽기 쉬운 내용이지만 여기서는 깊게 다루지 않겠습니다.

ViewDidLoad()메소드로 돌아가서 다음 코드를 추가합시다.

constraintsInit()

빌드를 하고 실행시키면 로그인 버튼이 화면 중앙에 위치한 것을 볼 수 있을겁니다. UI 요소들을 조금 더 추가해봅시다.

//add to viewDidLoad()
nameTextField = UITextField(frame: .zero)
nameTextField.placeholder = "Login Name"
nameTextField.borderStyle = .roundedRect
nameTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(nameTextField)

passwordTextField = UITextField(frame: .zero)
passwordTextField.placeholder = "Password"
passwordTextField.isSecureTextEntry = true
passwordTextField.borderStyle = .roundedRect
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(passwordTextField)

//add to constraintsInit()
passwordTextField.bottomAnchor.constraint(equalTo: loginButton.topAnchor, constant: -20),
passwordTextField.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 20),
passwordTextField.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor, constant: -20),

nameTextField.bottomAnchor.constraint(equalTo: passwordTextField.topAnchor, constant: -20),
nameTextField.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor, constant: -20)

빌드하고 실행시켜 보면 간단한 로그인 양식이 스토리보드 없이 만들어진 것을 볼 수 있습니다!

Adding Actions

스토리보드를 쓸 때 당신은 로그인 버튼에 IBAction을 연결하여 탭 이벤트를 처리해왔을겁니다. 우리는 인터페이스 빌더를 사용하지 않기때문에 IBAction의 장점이 없습니다. 대신에 우리는 target action을 사용할겁니다.

constraintsInit()함수 뒤에 새로운 함수를 추가해 줍시다.

@objc func handleLoginTouchUpInside() {
    print("Login has been tapped")
    if nameTextField.isFirstResponder {
       nameTextField.resignFirstResponder()
    }
    if passwordTextField.isFirstResponder {
       passwordTextField.resignFirstResponder()
    }
}

사용자가 텍스트 양쪽 텍스트 필드 모두에 포커스를 줄 수있기 때문에 간단히 각 텍스트필드에 첫번째 반응인지 질의를 할겁니다. 만약에 그렇다면 우리는 첫 응답상태를 그만둘 것입니다. 텍스트 필드에 대한 첫 응답상태를 그만두는 것은 키보드를 숨기는 것입니다. 물론 이것을 수행하는데 다른방법이 있지만 데모니까 이정도도 충분합니다.

이제 버튼에 대한 탭 액션을 추가합시다. viewDidLoad()메소드 안에 다음의 코드를 입력해주세요

loginButton.addTarget(self,
                      action: #selector(handleLoginTouchUpInside),
                      for: .touchUpInside)

이것은 로그인 버튼이 터치 업 인사이드 액션이 발생했을 때 탐지할 수 있도록 해줍니다.

Conclusion

보시다시피 스토리보드를 제거하는 것은 어렵지 않습니다. 스토리보드에 숨겨져 있었던 레이아웃과 뷰, 뷰컨트롤러들을 노출시키는 것입니다. 이 노출은 뷰들과 컨트롤러, 제약사항들이 어떻게 동작하는지 당신이 이해하는데 도움이 될 것입니다. 이제 프로그래머틱 레이아웃이라는 도두가 당신 손에 쥐어졌습니다. 모든 UI코드가 완전히 노출되고 리뷰와 디버깅이 훨씬 쉬워질 것입니다!

Summary

  • 스토리보드를 사용할 경우 협업을 하기에 문제가 많다.
  • 스토리보드, XIBs 충돌 문제 외에도 코드 리뷰시에 XML에서 값을 찾아봐야 하는 것이 불편하다.
  • 따라서 스토리보드를 없애고 코드로 레이아웃을 사용하는 것이 오류도 작고 리뷰하기에 편한 방식이다.
  • 혼자서 개발하는 것이 아니라면 스토리보드 없이 코딩하는 법을 알아야 한다.
  • 스토리보드를 사용하지 않으면 묵시적으로 수행되었던 UIKit의 여러가지 요소를 알게 된다.

© 2019. All rights reserved.

Powered by Hydejack v8.1.1