SwiftUI๊ฐ ์ฒ์ ๋์์ ๋, ์์ฃผ ์กฐ๊ธ UI์ ๊ด๋ จ๋ ๋ด์ฉ๋ค์ ํ์ตํ๋ ์ ์ด ์๋ค.
์๊ฐ์ด ์ผ๋ง๋ ์ง๋ฌ๋์ง
์ด์ ๋ ์ฑ์ฉ ๊ณต๊ณ ๋ค์ ๋ณด๋ฉด
SwiftUI์ Combine ๊ทธ๋ฆฌ๊ณ TCA๊ฐ ๊ธฐ์ ์คํ์ ๋ค์ด๊ฐ ์๋ ๊ฒ์
์๊ฐ๋ณด๋ค ์์ฃผ ๋ณผ ์ ์๋ค.
๊ทธ๋ ๋ค. ๋ฏธ๋ค๋๋ SwiftUI๋ฅผ ํ์ตํด์ผํ ๋๋ค.
์ฐ์
๊ณผ๊ฑฐ์ ํ์ตํ๋ ๊ฒ๋ค ์ค์ ๊ธฐ์ต์ ๋จ๋ ๊ฒ์ด ์ ํ ์๋ค.
์ญ์, ์ผ๋จ์ ๋ญ๋ผ๋ ๋ง๋ค์ด๋ด์ผ ๋ ๊ฒ ๊ฐ๋ค.
UIkit์ ํ์ตํ๋ฉด์ ๊ฐ๋จํ ๋ฉ๋ชจ์ฑ์ ๋ง๋ค์์๋๋ฐ
์ด๋ฒ์๋ ๋ง๋ค์ด ๋ด์ผ๊ฒ ๋ค.
๋ฉ๋ชจ์ฑ ๊ตฌ์กฐ
๊ฐ๋จํ๋ค.
๊ธฐ๋ณธํ๋ฉด
- ์์ฑ๋ ๋ฉ๋ชจ๋ค์ด ๋ฆฌ์คํธ ํํ๋ก ๋ณด์ด๋ ํ๋ฉด + ๋ฉ๋ชจ๋ฅผ ๊ฒ์ํ ์ ์๋ SearchBar
์์ ๋ฐ ์ถ๊ฐ ํ๋ฉด
- ๋ฆฌ์คํธ์ ์๋ ์์ดํ ์ ํด๋ฆญํด์ ๋ค์ด์ค๊ฒ๋๋ฉด ์์
- ์๋จ์ Toolbar์์ ์ถ๊ฐํ๊ธฐ ๋ฒํผ์ผ๋ก ๋ค์ด์ค๊ฒ ๋๋ฉด ์ถ๊ฐ
์ํคํ ์ฒ
TCA๋ SwifUI์์ UIKit์ MVVM ๋งํผ ์ฃผ๋ฅ๊ฐ ๋ ์ํคํ ์ฒ์ธ ๊ฒ ๊ฐ๋ค.
๋์ธ์๋ ์ด์ ๊ฐ ์๋ค๊ณ ์๊ฐํ๋ค.
์ผ๋จ ์ฌ์ฉํด๋ณด๋ฉด์ ๋๋ ๊ฑด UIKit์์ ์ฌ์ฉํ๋ ReactorKit๊ณผ ๊ฑฐ์ ์ ์ฌํ ๊ฒ ๊ฐ๋ค.
์๋, ์ฐจ์ด์ ์ ๊ทธ๋ค์ง ๋๋ผ์ง ๋ชปํ๋ค.
๊ฐ๋จํ๊ฒ ์ดํดํ๋๋ก ์ค๋ช ํด ๋ณด๋ฉด
๋จ๋ฐฉํฅ ํ๋ก์ฐ์ ์ํคํ ์ฒ๋ก ์ก์ - ์ํ ๋ณ๊ฒฝ - UI ๋ณ๊ฒฝ ์ฌ์ดํด๋ก ์ด๋ฃจ์ด์ง๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
๊ตฌํ
1. ์ผ๋จ List์ ๋ฃ์ ๋ฉ๋ชจ ์์ดํ ์ด ํ์ํ๋ค.
struct Memo: Identifiable, Equatable {
var id = UUID()
var title: String
var description: String
var date: Date
}
List์ ์์ดํ ์ Identifiable ํด์ผ ํ๋ค.
2. List์ Row์ ๋ค์ด๊ฐ ๋ฉ๋ชจ๋ทฐ๊ฐ ํ์ํ๋ค.
struct MemoView: View {
var memo: Memo
init(memo: Memo) {
self.memo = memo
}
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 12) {
Text(memo.title)
.font(.system(size: 18, weight: .bold))
.lineLimit(2)
.foregroundStyle(.white)
Text(memo.description)
.font(.system(size: 16, weight: .regular))
.lineLimit(2)
.foregroundStyle(.white)
}
.padding()
.background(.black)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(.black)
.cornerRadius(12)
}
}
์์ง UI๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐฉ์์ด ๋ณต์กํ๊ณ , ์ด๋ ต๊ฒ ๋๊ปด์ง๋ค.
์ฒ์ MemoView๋ฅผ ๋ฆฌ์คํธ์ ์ ์ฉํ์ ๋, Width๊ฐ ํ๋ฉด๋๋น์ ์ ๋ฐ๋ ํ์๋์ง ์์์๋ค.
UIKit์ ์ฌ์ฉํ๋ ์ ์ ๋ผ๋ฉด .frame() ์ฝ๋๋ฅผ ์ ๋ด๋๋ฉด ์ข๋ค.
์ต์ ๋๋น๋ฅผ 0, ์ต๋ ๋๋น๋ฅผ .infinity๋ก ์ค์ ํ๊ฒ ๋๋ฉด
UIkit์ UITableView์์ Cell์ ๊ฝ ์ฐจ๊ฒ Constraint๋ฅผ ์ฃผ์๋ ๊ฒ์ฒ๋ผ ๋ง๋ค ์ ์๋ค.
3. List์๋ Memo ๋ฐฐ์ด์ ์ ๋ฌํด์ผ ํ๋ค. ๋ฐ๋ผ์ ์ด Memo ๋ฐฐ์ด์ ์ํ๋ก ๊ฐ์ง๊ณ ์๋ Memos Reducer๋ฅผ ๋ง๋ค๋ฉด ๋๋ค.
struct Memos: Reducer {
struct State: Equatable {
var memos: [Memo] = []
var searchedMemos: [Memo] = []
var selectedMemo: Memo? = nil
}
enum Action: Equatable {
case findMemo(_ content: String)
case addMemo(_ memo: Memo)
case deleteMemo(_ id: UUID)
case updateMemo(id: UUID, title: String, description: String)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .findMemo(let content):
state.searchedMemos = state.memos.filter { $0.description.contains("\(content)") || $0.title.contains("\(content)") }
return .none
case .addMemo(let memo):
state.memos.append(memo)
return .none
case .deleteMemo(let id):
state.memos = state.memos.filter { $0.id != id }
return .none
case .updateMemo(id: let id, title: let title, description: let description):
if let index = state.memos.firstIndex(where: { $0.id == id }) {
state.memos[index].title = title
state.memos[index].description = description
state.memos[index].date = Date()
}
return .none
}
}
}
}
State๋ฅผ ๋ณด๋ฉด
ํ๋ฉด์ ๋ณด์ฌ์ค Memo ๋ฐฐ์ด์ ๊ฐ์ง๊ณ ์๋ค. ๋, List์ Row๋ฅผ ํด๋ฆญํด ์์ ์ ์ํ ๋ ์ฌ์ฉํ SelectedMemo ํ๋กํผํฐ์ ๊ฒ์ ์ ์ฌ์ฉํ Memo ๋ฐฐ์ด๋ ๊ฐ์ง๊ณ ์๋ค. ( Realm์ด๋ SQLite์ ๊ฐ์ DB๊ด๋ จ ์ฝ๋๊ฐ ์๊ธฐ ๋๋ฌธ์ Memo ๋ฐฐ์ด์์ ํํฐ๋งํด์ ๊ฐ์ ธ์์ผ ํ๋ค. )
1. findMemo(_ content: String) - description์ ํด๋น String์ ํฌํจํ๋ ๋ฉ๋ชจ๊ฐ ์๋ค๋ฉด searchedMemos์ ์ ์ฅํ๊ธฐ
2. addMemo(_ memo: Memo) - memos ๋ฐฐ์ด์ ๋ฉ๋ชจ ์ถ๊ฐ
3. deleteMemo(_ id: UUID) - id๊ฐ ๋์ผํ ๋ฉ๋ชจ๋ฅผ ์ฐพ์ ์ ๊ฑฐ
4. updateMemo(id: UUID, title: String, description: String) - id๊ฐ ์ผ์นํ๋ ๋ฉ๋ชจ๋ฅผ ์ฐพ๊ณ ๋ด์ฉ ์ ๋ฐ์ดํธ
4. ์ฌ๊ธฐ๊น์ง ๊ตฌ์ฑํ๋ค๋ฉด MemosView๋ง ๊ตฌํํ๋ฉด ๋๋ค.
struct MemosView: View {
let store: StoreOf<Memos>
@State private var isShowingBottomSheet = false
@State private var bottomSheetType: BottomSheetType = .add(.fail)
@State private var searchedText: String = ""
@State private var isSearching: Bool = false
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
ZStack {
NavigationStack {
List {
ForEach(memoList(for: viewStore), id: \.id) { memo in
NavigationLink {
MemoEditor(memo: memo, memoEditType: .update) { memo in
viewStore.send(.updateMemo(id: memo.id, title: memo.title, description: memo.description))
bottomSheetType = .update(.success)
isShowingBottomSheet.toggle()
}
} label: {
MemoView(memo: memo)
}
}
.onDelete(perform: { indexSet in
let index = indexSet[indexSet.startIndex]
let id = viewStore.state.memos[index].id
viewStore.send(.deleteMemo(id))
bottomSheetType = .delete(.success)
isShowingBottomSheet.toggle()
})
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.listStyle(.grouped)
.navigationTitle("๋ฉ๋ชจ")
.toolbar {
NavigationLink(destination: {
MemoEditor(memo: nil, memoEditType: .add) { memo in
viewStore.send(.addMemo(memo))
bottomSheetType = .add(.success)
isShowingBottomSheet.toggle()
}
}, label: {
Text("์ถ๊ฐํ๊ธฐ")
.font(.system(size: 18, weight: .regular))
.foregroundStyle(.black)
})
}
}
.modifier(NavigationBarStyleModifier())
.searchable(text: $searchedText, placement: .navigationBarDrawer, prompt: "๋ฉ๋ชจ๋ฅผ ๊ฒ์ํด๋ณด์ธ์.")
.onChange(of: searchedText) {
isSearching = !searchedText.isEmpty
viewStore.send(.findMemo(searchedText))
}
BottomSheet(isShowing: $isShowingBottomSheet, bottomSheetType: $bottomSheetType)
}
}
}
private func memoList(for viewStore: ViewStore<Memos.State, Memos.Action>) -> [Memo] {
if isSearching {
return viewStore.state.searchedMemos
} else {
return viewStore.state.memos
}
}
}
์ฒ์ ์์ฑํด ๋ด์ ๊ทธ๋ฐ์ง ์กฐ๊ธ์ ์ด์คํผ ๋ณด์ผ ์ ์๋ค. ์๋ ์ด์คํ๋ค.
์ค๊ฐ์ค๊ฐ modifier๋ BottomSheet ๊ฐ์ ๊ฒฝ์ฐ์๋ ๋ฐ๋ก ํฌ์คํ ํ ์์ ์ด๋ค.
์ฝ๋ ์ค๋ช
1. ZStack์ ์ฌ์ฉํ ์ด์ ๋ BottomSheet์ hierarchy ๋๋ฌธ์ด๋ค. ๋ค์ ํฌ์คํ ์์ ํด๋น ๋ด์ฉ์ ๋ํด ์์ธํ๊ฒ ์์ฑํด์ผ๊ฒ ๋ค.
2. TCA์์ ์ ๊ณตํ๋ WithViewStore๋ฅผ ํตํด ์์์ ์์ฑํ๋ Memos Reducer๋ฅผ ๊ด๋ฆฌํ๋ Store๋ฅผ ์ธ์๋ก ๋ฃ๊ณ , ์ํ๋ฅผ observe ํ๋ฉด ๋๋ค. ( ์ ํํ์ง ์์ง๋ง store๋ ์ปค๋งจ๋์ผํฐ๋ผ๊ณ ๋ณด๋ฉด ๋ ๊ฒ ๊ฐ๋ค. )
3. List์ ์์ดํ ์ ๋๊ฒจ์ค ๋, memoList๋ผ๋ ๋ฉ์๋์ ๋ฐํ๊ฐ์ ๋ฃ์ด์ค๋ค. ๊ฒ์ ์ค์ผ ๊ฒฝ์ฐ์๋ ๊ฒ์๋ ๋ฉ๋ชจ๋ง ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์์ด๋ค. ๊ฒ์ ์ค์ด ์๋๋ผ๋ฉด ๊ธฐ์กด์ memo ๋ฐฐ์ด์ ๋ณด์ฌ์ฃผ๋ฉด ๋๋ค.
4. List์ ์ญ์ ๋ onDelete๋ฅผ ํตํด์ ์ด๋ฃจ์ด์ง๋ค. ์ฌ๊ธฐ์ ์ ๊ฑฐํ๊ณ ์ ํ๋ ์ธ๋ฑ์ค๋ฅผ ๊ฐ์ง๊ณ ์ action(.delete(id))์ ๋ณด๋ด๋ฉด ์ํ๊ฐ ๋ฐ๋๊ฒ ๋๊ณ View๊ฐ ์ ๋ฐ์ดํธ๋๋ค.
์ ๋ฆฌ
TCA์ ๋ํด ์กฐ๊ธ์ ์๊ฒ ๋ ๊ฒ ๊ฐ๊ณ , ๊ธฐ์กด๋ณด๋ค UI๋ฅผ ๊ตฌํํ ๋, ์กฐ๊ธ์ ๊ฐ์ด ์๊ธด ๊ฒ ๊ฐ๊ธฐ๋ ํ๋ค.
๊ทธ๋๋ ์ฌ์ ํ ๊ฐ๊ธธ์ด ๋ฉ๋ค. ์กฐ๊ธ ๋ ๋ณต์กํ ๋ก์ง์ ๊ตฌํํด์ผ ํ๋ ์ฑ์์๋ ํค๋งฌ ์๋ ์์ ๊ฒ ๊ฐ๋ค.
3์ ์ ํ๋ก ์ฑ์ ํ๋ ์ ๋๋ก ๋ง๋ค์ด๋ด์ผ๊ฒ ๋ค.
'iOS > SwiftUI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[SwiftUI] ์ปค์คํ BottomSheet์ ๋ง๋ค์ด๋ณด์ (0) | 2024.01.22 |
---|