Ref)
https://pilgwon.github.io/post/episode-65-swiftui-and-state-management-part-1
[번역] Point-Free #65 SwiftUI와 상태 관리 - 파트 1
거대하고 복잡한 아키텍쳐를 만드는 일엔 어떤 장애물이 도사리고 있을까요? 오늘은 SwiftUI로 앱을 만들어보면서 애플은 그 장애물을 어떻게 극복했는지 알아보려고 합니다.
pilgwon.github.io
https://pilgwon.github.io/post/episode-66-swiftui-and-state-management-part-2
[번역] Point-Free #66 SwiftUI와 상태 관리 - 파트 2
오늘은 파트 1에서 만든 어플리케이션에 약간의 사이드 이펙트와 화면을 추가해서 어플리케이션을 완성해봅시다. 그 후에
pilgwon.github.io
https://pilgwon.github.io/post/episode-67-swiftui-and-state-management-part-3
[번역] Point-Free #67 SwiftUI와 상태 관리 - 파트 3
지난 두 에피소드에서 SwiftUI 어플리케이션을 만들면서 이런 생각이 드셨을 것입니다.
pilgwon.github.io
(본 포스트는 위에 블로그를 참고하여 코드를 새로운 버전으로 리팩토링한 글입니다.)
완성화면
네비게이션 뷰
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: EmptyView()) {
Text("Counter demo")
}
NavigationLink(destination: EmptyView()) {
Text("Favorite primes")
}
}
.navigationTitle("State management")
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
가장 먼저 네비게이션 화면을 만들어봤으며 원 글과 같이 Playground에서 코드를 작성하고 실행했다.
NavigationView에 타이틀을 달기 위해서는 .navigationTitle을 사용하면 되는데 이는 NavigationView 안 최상단 즉, 위 코드에서는 List에 달아서 사용해야 된다.
또 각각의 뷰는 아직 구성 전이므로 임시로 EmptyView()를 사용하였다.
카운터 뷰
- UI 구성하기
struct CounterView: View {
var body: some View {
VStack {
HStack {
Button(action: {}) {
Text("-")
}
Text("0")
Button(action: {}) {
Text("+")
}
}
Button(action: {}) {
Text("Is this prime?")
}
Button(action: {}) {
Text("What is the 0th prime?")
}
}
.navigationTitle("Counter demo")
}
}
버튼에 action을 구현하지 않은 카운터 뷰의 UI만 간단한게 구성해봤다.
이제 새로운 뷰 구조체를 만들었으니 기존에 있던 NavigationView의 카운터 뷰 destination으로 설정하고 실행하면 잘 동작되는 것을 확인할 수 있다.
NavigationLink(destination: CounterView()) { // 👈 CounterView() 추가
Text("Counter demo")
}
// ContentView.swift
- 기본 기능 구현하기
struct CounterView: View {
@State private var count = 0 // 👈 현재 카운트 숫자
var body: some View {
VStack {
HStack {
Button(action: { count -= 1 }) { // 👈 현재 카운트 숫자 - 1
Text("-")
}
Text("\(count)") // 👈 현재 카운트 숫자
Button(action: { count += 1 }) { // 👈 현재 카운트 숫자 + 1
Text("+")
}
}
Button(action: {}) {
Text("Is this prime?")
}
Button(action: {}) {
Text("What is the \(ordinal(count)) prime?")
}
}
.navigationTitle("Counter demo")
}
// 👇 1st, 2nd, 3th를 표시하기 위한 함수 추가
private func ordinal(_ n: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
return formatter.string(for: n) ?? ""
}
}
이제 UI에 동작을 구현해보겠습니다. SwiftUI는 State 프로퍼티 래퍼를 통해 변수를 변경하고 뷰를 다시 그린다.
위 코드에서 -나 +버튼을 클릭하면 count값이 변경되어 뷰를 다시 그리고 Text에 변경된 값이 표시된다.
카운터 화면 상태 유지하기
위 화면은 문제없이 동작해 보이지만 카운터 뷰를 나갔다가 돌아오면 CounterView가 다시 생성되어 count가 다시 0부터 시작되는 문제가 있습니다. 이제 값 부분을 따로 관리하여 count를 유지하는 방법을 알아봅시다.
class AppState: ObservableObject {
@Published var count: Int = 0
}
가장 먼저 ObservableObject를 준수하는 클래스를 만들어 줍시다. 그리고 저장할 값인 count 변수를 생성하고 값 변화를 감지하여 뷰에 알려주기 위해 @Published 프로퍼티 래퍼를 사용하여 감싸줍니다.
(이에 대한 자세한 설명은 원문을 참조해 주시길 바랍니다!)
그리고 기존 CounterView를 수정해 줍니다.
struct CounterView: View {
@ObservedObject var state: AppState // 👈 기존 변수 count를 AppState로 변경
var body: some View {
VStack {
HStack {
Button(action: { state.count -= 1 }) { // 👈 이제 AppState의 count의 값을 가리킨다.
Text("-")
}
Text("\(state.count)") // 👈
Button(action: { state.count += 1 }) { // 👈
Text("+")
}
}
Button(action: {}) {
Text("Is this prime?")
}
Button(action: {}) {
Text("What is the \(ordinal(state.count)) prime?") // 👈
}
}
.navigationTitle("Counter demo")
}
private func ordinal(_ n: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
return formatter.string(for: n) ?? ""
}
}
그 다음 CounterView가 AppState를 주입 받아야하기 때문에 ContentView도 수정해준다.
struct ContentView: View {
@ObservedObject var state: AppState // 👈 ContentView도 AppState를 주입받는다.
var body: some View {
NavigationView {
List {
NavigationLink(destination: CounterView(state: state)) { // 👈 CounterView에 state를 주입해 준다.
Text("Counter demo")
}
NavigationLink(destination: EmptyView()) {
Text("Favorite primes")
}
}
.navigationTitle("State management")
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView:
ContentView(state: AppState()) // 👈 ContentView에 AppState를 생성하여 주입해준다.
)
이제 카운터뷰의 값은 화면이 바뀌어도 상태를 유지하여 뷰가 다시 생성되어도 변경된 값을 가지고 있을 수 있다.
소수 확인 모달
SwiftUI는 모달을 띄위기 위한 여러 메소드들이 있는데 그 중 .sheet를 사용하여 소수 확인 모달을 구현해 보자
struct CounterView: View {
@ObservedObject var state: AppState
@State private var isPrimeModalShown: Bool = false // 👈 소수 확인 모달을 띄울 트리거가 되는 값
var body: some View {
VStack {
HStack {
Button(action: { state.count -= 1 }) {
Text("-")
}
Text("\(state.count)")
Button(action: { state.count += 1 }) {
Text("+")
}
}
Button(action: { isPrimeModalShown = true }) { // 👈 버튼을 클릭하면 변수를 true로 변경시켜 모달을 띄운다.
Text("Is this prime?")
}
.sheet(isPresented: $isPrimeModalShown) {
IsPrimeModalView(state: state)
}
// 👆 .sheet의 isPresented에 Binding 변수 값을 사용하여 IsPrimeMadalView를 띄운다.
Button(action: {}) {
Text("What is the \(ordinal(state.count)) prime?")
}
}
.navigationTitle("Counter demo")
}
private func ordinal(_ n: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
return formatter.string(for: n) ?? ""
}
}
소수인지 확인하는 메소드를 작성하고 그에 따라 달라지는 뷰를 구성한다.
struct IsPrimeModalView: View {
@ObservedObject var state: AppState
var body: some View {
VStack {
if isPrime(state.count) {
Text("\(state.count) is prime 🎉")
Button(action: {}) {
Text("Save/remove to/from favorite primes")
}
} else {
Text("\(state.count) is not prime :(")
}
}
}
private func isPrime(_ p: Int) -> Bool {
if p <= 1 { return false } // 1은 소수가 아니다.
if p <= 3 { return true } // 2, 3은 무조건 소수다.
for i in 2...Int(sqrtf(Float(p))) {
if p % i == 0 { return false }
}
return true
}
}
소수 확인 모달UI는 완성했으니 소수를 저장하는 로직을 구현해보겠습니다.
먼저 AppState 클래스에 소수를 저장할 배열 변수를 추가합니다.
class AppState: ObservableObject {
@Published var count: Int = 0
@Published var favoritePrimes: [Int] = [] // 👈 배열 변수 추가
// 👇 현재 숫자를 저장했는지 확인하는 변수 추가
var hasFavoritePrime: Bool {
favoritePrimes.contains(count)
}
}
기존 모달 뷰에서 저장한 값인지에 따라 달라지는 뷰를 다시 구성해 보고 기능도 추가해 봅시다.
...
if isPrime(state.count) {
Text("\(state.count) is prime 🎉")
// 👇 해당 숫자를 이미 저장했을 경우 삭제하고 아닐경우 저장할 수 있는 뷰와 기능을 추가한다.
if state.hasFavoritePrime {
Button(action: { state.favoritePrimes.removeAll { $0 == state.count } }) {
Text("Remove from favorite primes")
}
} else {
Button(action: { state.favoritePrimes.append(state.count) }) {
Text("Save to favorite primes")
}
}
} else {
Text("\(state.count) is not prime :(")
}
...
// IsPrimeModalView.swift
n번째 소수 찾기 ( pass )
즐겨찾기 뷰
struct FavoritePrimes: View {
@ObservedObject var state: AppState // 👈 즐겨찾기 뷰 역시 AppState를 주입 받는다.
var body: some View {
// 👇 List와 ForEach로 저장한 소수 목록을 보여준다.
List {
ForEach(state.favoritePrimes, id: \.self) { prime in
Text("\(prime)")
}
// 👇 onDelete 메소드를 통해 각 리스트를 제거하는 기능을 쉽게 추가할 수 있다.
.onDelete { indexSet in
for index in indexSet {
state.favoritePrimes.remove(at: index)
}
}
}
.navigationBarTitle(Text("Favorite Primes"))
}
}
struct ContentView: View {
@ObservedObject var state: AppState
var body: some View {
NavigationView {
List {
NavigationLink(destination: CounterView(state: state)) {
Text("Counter demo")
}
NavigationLink(destination: FavoritePrimes(state: state)) { // 👈 destination을 즐겨찾기 뷰로 생성
Text("Favorite primes")
}
}
.navigationTitle("State management")
}
}
}
즐겨찾기 뷰 구조체를 만들었다면 ContentView로 돌아와서 NavigationLink의 destination에 추가하여 뷰 전환을 한다.
이제 카운터 뷰에서 저장한 소수들을 즐겨찾기 뷰에서 리스트로 확인 할 수 있고 삭제할 수도 있게 되었다.
마무리
여기까지 기본적인 기능이 있는 앱을 만들면서 기본적인 기능을 만들어봤다. 위 링크의 part-1과 part-2에 해당하는 내용이고 part-3은 SwiftUI의 문제점등에 대한 내용이여서 여기에서는 추가하지 않았지만 좋은 내용이니 한번 확인해 보는 것도 좋을 것같다. 이 프로젝트를 진행하면서 SwiftUI의 동작구조나 상태관리등을 간단하게 알아 볼 수 있는 계기가 되었던 것 같다 :)
'iOS > SwiftUI' 카테고리의 다른 글
SwiftUI 그라데이션 텍스트 구현하기 (0) | 2022.04.27 |
---|---|
SwiftUI에서 UITextField 커스텀하여 사용하기 (0) | 2022.04.10 |
SwiftUI에서 스켈레톤 UI 적용하기 (0) | 2022.02.20 |
[SwiftUI] EnvironmentObject / ObservedObject / StateObject (0) | 2022.01.30 |
[Swift] Codable로 JSON파싱하기 -확장- (0) | 2022.01.30 |