SwiftUI 튜토리얼 1편 — 기본구조
최근 SwiftUI를 공부하며 정리한 내용들을 복습하고자 블로깅하게되었습니다. Swift UI는 UIKit 위에서 빌드되는 프레임워크로 새로운 방식의 UI 인터페이스를 제공하며 선언형 방식의 구조를 가지고 있습니다. 현업에서는 스토리보드를 쓰는 경우가 드물고 뷰마다 일일이 가독성 떨어지는 constraint를 도배해가며 작업하는 경우가 대부분인데 정말 희소식이 아닐까 싶습니다.
Xcode에서 SwiftUI Preview는 카탈리나 버전부터 사용할 수 있지만 본 튜토리얼을 위해서 업그레이드를 하실 필요는 없습니다.
프로젝트의 구조
프로젝트를 생성할 때 SwiftUI를 체크하고 만들게 되면 이전에는 보지 못했던 SceneDelegate.swift와 ContentView.Swift 마지막으로 Preview Content 폴더가 보입니다. (SwiftUI를 체크하지 않아도 SceneDelegate 파일은 만들어집니다.)
- SceneDelegate.swift
SceneDelegate는 iPadOS의 멀티 윈도와 같은 기능을 지원하기 위한 Delegate으로 동일한 앱을 여러 화면에 띄울 경우 AppDelegate이 앱의 전역에서 작동하는데 반해 SceneDelegate는 각 화면의 인스턴스 단위로 작동시킬 수 있습니다.
- ContentView.swift
맨 처음 생성되는 Simulator를 실행하면 출력되는 View로 SceneDelegate클래스의 Scene 메서드에 선언되어있습니다.
window.rootViewController = UIHostingController(rootView: contentView)
UIHostingController는 SwiftUI View를 인자로받아 ViewController를 만들어줍니다.
- Preview Content
Xcode11 버전부터 Xcode에서 Canvas 기능을 통해 Simulator 없이도 Xcode에서 화면을 미리 볼 수 있게 되었습니다. Canvas에서 사용되는 데이터들을 위한 Assets입니다.
ContentView 파일을 확인하면 맨 상단 import SwiftUI를 선언하여 프레임워크를 가져오고 View를 상속받는 ContentView 구조체가 선언되어있습니다.
ContentView 내부를보면 body가 View 타입으로 선언되어있지만 some 이라는 키워드가 보입니다. some은 Swift 5.1에서 생긴 키워드로 찾아볼수록 명쾌하게 설명할 자신이 없어서 정말 궁금하신분들은 SE-0244 제안서를 보길 추천드립니다. 간단히 특정한 조건을 만족하는 제네릭 타입의 View라고 넘어가겠습니다.
SwiftUI에서 View는 반드시 body 변수가 있어야하며 최상위 View 역활을합니다.
Canvas
하단 ContentView_Preview는 실제 앱에는 적용되지 않지만 Xcode의 Canvas 기능을 위한 Preview Layout입니다.
Xcode11 이상버전이라면 Canvas 윈도우를 활성화 시킬 수 있습니다. Xcode 11버전 미만이시라면 Simulator를 키셔도 동일하게 표시가 됩니다.
상단 [Resume]버튼을 누르면 내가 만든 View를 실시간으로 확인할 수 있습니다. 이외에도 canvas에 렌더링된 view에 GUI 인터페이스를 이용해서 view를 추가할수도 있습니다.
var body: some View {Form{
Text("Hello World")
}
이번엔 직접 View를 다루어보겠습니다. Form을 선언하고 기존의 TextView를 감싸줍니다. 그리고 실행해보겠습니다.
배경이 회색으로 바뀌고 텍스트뷰가 상단으로 올라간걸 볼 수 있습니다. Form은 주로 데이터나 설정등을 다루기위해 사용하는 컨테이너입니다. 이번에는 TextView를 10개 추가해보겠습니다.
Form{ Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")}
10개까지를 추가하고 실행하면 문제가 없지만 만약 11개를 추가하게되면 아래와같은 에러가 발생합니다.
Swift UI에서 최상위 View는 최대 10개의 Child View를 가질 수 있고 만약 10개를 초과하게된다면 아래와같이 다른 태그를 이용하여 감싸주어야합니다.
Form{Text("Hello World")Group{
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
}
Group{ Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")
Text("Hello World")}}}
Group 이외에도 Stack 컨테이너를 이용해서 묶어줄 수 있습니다.
NavigationView{
Form{
Text("Hello World")
}
}
이번에는 Form 컨테이너를 NavigationView 컨테이너로 감싸겠습니다. Navigation View는 기존의 UINavigationController 역할을 하는 컨테이너입니다. 그리고 실행하게 되면 상단에 Title과 Navigation을 위한 공간이 생깁니다.
NavigationView{
Form{
Text("Hello World")
}.navigationBarTitle("this is title")
}
Form블록 끝에 navigationBarTitle을 추가하고 String을 인자로 줍니다.
실행하면 Safe Area 하단으로 타이틀이 생긴 것을 볼 수 있습니다. 한 가지 주의할 점은 NavigationView태그의 끝이 아닌 Form태그의 끝에 navigationBarTitle 메서드를 작성해야 합니다.
NavigationView{
Form{
Text("Hello World")
Button("this is Button"){ //버튼을 눌렀을경우 발생하는 callback
}
}.navigationBarTitle("this is title")
}
이번에는 Button을 추가해보겠습니다. Button은 String을 인자로 받고 버튼을 눌렀을 때의 콜백을 클로저 블록 안에 작성할 수 있습니다. 실행 후 이상이 없는지 한번 체크해주세요.
만약 우리가 버튼을 누를때마다 TextView의 텍스트가 바뀌어야한다면 메모리에 데이터를 가지고있어야하고 그러기위해 변수로 선언이 되어야합니다.
하지만 구조체 내에 변수를 선언하고 Button의 콜백에서 Self.touchedCount에 1을 증가시키게 되면 에러가 발생하는데 구조체의 특징상 내부 메서드 안에서 자신의 변수를 변경할 수 없기 때문입니다. Swift 5.1에서는 이런 문제를 해결하기 위해 State 키워드가 만들어졌습니다.
@State var touchedCount = 0
touchedCount 변수에 State 어노테이션을 선언해주면 정상적으로 실행할 수 있습니다. SwiftUI에서는 State 어노테이션이 붙은 변수에 변경이 일어나면 자동으로 View를 다시 렌더링하게됩니다.
2편에서는 양방향 데이터바인딩을 다루어보겠습니다.