NewCategoryCore.swift
import Foundation
import ComposableArchitecture
public enum NewCategoryViewAlert: Int, Identifiable {
case name = 0
case icon = 1
var text: String {
switch self {
case .name:
return "Lege einen Namen fest!"
case .icon:
return "Wähle ein Icon aus!"
}
}
public var id: Int {
return self.rawValue
}
}
public struct NewCategoryState: Equatable {
/// The name of the selected icon
var selectedIcon: String = ""
/// The name of the new category
var categoryName: String = ""
/// The alert of the view
var alert: NewCategoryViewAlert?
}
enum NewCategoryAction: Equatable {
case saveSpace
case categoryNameChanged(String)
case selectedIconChanged(String)
case alertChanged(NewCategoryViewAlert?)
}
let newCategoryReducer = Reducer<NewCategoryState, NewCategoryAction, Void> { state, action, _ in
switch action {
case .saveSpace:
// MARK: TODO save
return .none
case let .categoryNameChanged(name):
state.categoryName = name
return .none
case let .selectedIconChanged(icon):
state.selectedIcon = icon
return .none
case let .alertChanged(alert):
state.alert = alert
return .none
}
}.debug()
NewCategoryView.swift
import SwiftUI
import ComposableArchitecture
struct NewCategoryView: View {
@Environment(\.presentationMode) var isPresented
/// Store
let store: Store<NewCategoryState, NewCategoryAction>
/// ViewStore
@ObservedObject var viewStore: ViewStore<NewCategoryState, NewCategoryAction>
init() {
self.store = Store(
initialState: NewCategoryState(),
reducer: newCategoryReducer,
environment: { }())
self.viewStore = ViewStore(self.store)
}
var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 16) {
Group {
CategoryName
Divider()
}
.padding(.horizontal)
IconList
}
.navigationBarTitle("Neue Kategorie", displayMode: .inline)
.navigationBarItems(trailing: self.trailingItem())
.bottomButton(buttonTitel: "Hinzufügen") {
self.save()
}
.alert(item: self.viewStore.binding(
get: { $0.alert },
send: NewCategoryAction.alertChanged)
) { (alert) -> Alert in
Alert(title: Text(alert.text))
}
}
}
var CategoryName: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Name")
Text("Gib einen Namen für die Kategorie an")
TextField("z.B. Lebensmittel & Gastronomie",
text: self.viewStore.binding(
get: { $0.categoryName },
send: NewCategoryAction.categoryNameChanged
)
)
.keyboardType(.alphabet)
.padding(.horizontal)
.frame(height: 50, alignment: .center)
.background(Color.tertiarySystemFill)
.cornerRadius(8)
}
}
var IconList: some View {
let rows: Int
if icons.count % 6 != 0 {
rows = (icons.count / 6) + 1
} else {
rows = (icons.count / 6)
}
return VStack(alignment: .leading, spacing: 8) {
Group {
Text("Icon")
.padding(.horizontal)
Text("Wähle ein Icon aus")
.padding(.horizontal)
}
ScrollView(.horizontal, showsIndicators: false) {
ForEach(0..<6) { y in
HStack(alignment: .center, spacing: 8) {
ForEach(0..<rows) { x in
IconButton(
index: x * 6 + y,
selectedIcon:
self.viewStore.binding(
get: { $0.selectedIcon },
send: NewCategoryAction.selectedIconChanged
)
)
}
}.padding(.horizontal)
}.padding(.vertical, 2)
}
}
}
struct IconButton: View {
/// The index in the icon list (String)
let index: Int
/// The name of the selected icon
@Binding var selectedIcon: String
var body: some View {
if index < icons.count {
let icon: String = icons[index]
return Button(action: {
self.selectedIcon = icon
}) {
Image(systemName: icon)
.foregroundColor(.label)
.frame(width: 69, height: 50)
.background(Color.tertiarySystemFill)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: icon == selectedIcon ? 4 : 0)
)
.cornerRadius(8)
}.eraseToAnyView()
} else {
return
Spacer()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50, maxHeight: 50)
.eraseToAnyView()
}
}
}
private func save() {
self.isPresented.wrappedValue.dismiss()
}
private func trailingItem() -> some View {
return Button(action: {
self.isPresented.wrappedValue.dismiss()
}) {
Text("Abbrechen")
}
}
}
struct NewCategoryView_Previews: PreviewProvider {
static var previews: some View {
NewCategoryView()
}
}
public static let tertiarySystemFill: Color = Color(UIColor.tertiarySystemFill)
extension View {
func eraseToAnyView() -> AnyView {
AnyView( self )
}
}
public let icons = ["airplane",
"alarm",
"antenna.radiowaves.left.and.right",
"app.gift.fill",
"archivebox.fill",
"bag.fill",
"bandage.fill",
"barcode",
"bed.double.fill",
"bell.fill",
"book.fill",
"briefcase.fill",
"calendar",
"camera.on.rectangle.fill",
"car.fill",
"cart.fill",
"creditcard.fill",
"cube.box.fill",
"desktopcomputer",
"doc.fill",
"doc.text.fill",
"envelope.fill",
"envelope.open.fill",
"eyeglasses",
"film",
"gamecontroller.fill",
"gear",
"gift.fill",
"globe",
"guitars",
"hammer.fill",
"hare.fill",
"headphones",
"heart.fill",
"hifispeaker.fill",
"house.fill",
"map.fill",
"mic.fill",
"music.mic",
"music.note.list",
"paintbrush.fill",
"paperclip",
"paperplane.fill",
"pencil",
"pencil.and.outline",
"person.2.square.stack.fill",
"person.crop.rectangle.fill",
"phone.fill",
"photo.fill",
"printer.fill",
"shield.fill",
"signature",
"snow",
"speedometer",
"sportscourt.fill",
"stopwatch.fill",
"studentdesk",
"tag.fill",
"thermometer",
"tram.fill",
"trash.fill",
"tv.fill",
"umbrella.fill",
"wrench.fill",
"zzz"]
Hope that helps.