Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions NativeAppTemplate/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ enum NativeAppTemplateConstants {
static let shadowRadius: CGFloat = 8
}

// MARK: - Shop

static let maximumShopNameLength = 100
static let maximumShopDescriptionLength = 1_000

// MARK: - ItemTag

static let maximumItemTagNameLength = 100
Expand Down Expand Up @@ -146,6 +151,17 @@ extension String {
static let addShopDescription = "Add a new shop."
static let deleteShop = "Delete Shop"
static let shopNameIsRequired = "Shop name is required."
static let shopNameIsInvalid = "Shop name is invalid."
static let shopDescriptionIsInvalid = "Shop description is too long."

static func shopNameHelp(maximumLength: Int) -> String {
"Name must be 1–\(maximumLength) characters."
}

static func shopDescriptionHelp(maximumLength: Int) -> String {
"Description can be up to \(maximumLength) characters."
}

static let timeZone = "Time Zone"
static let createShopsLabel = "Create shops"
static let tapShopBelow = "Tap a shop below."
Expand Down
27 changes: 25 additions & 2 deletions NativeAppTemplate/UI/Shop List/ShopCreateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,37 @@ struct ShopCreateView: View {
Form {
Section {
TextField(String.name, text: $viewModel.name)
.onChange(of: viewModel.name) {
viewModel.validateNameLength()
}
} header: {
Text(String.shopName)
} footer: {
Text(String.shopNameIsRequired)
.foregroundStyle(viewModel.hasInvalidData ? .validationError : .clear)
VStack(alignment: .leading) {
Text(String.shopNameHelp(maximumLength: viewModel.maximumNameLength))
.font(.uiFootnote)
Text(String.shopNameIsInvalid)
.font(.uiFootnote)
.foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear)
}
}

Section {
TextField(String.descriptionString, text: $viewModel.description, axis: .vertical)
.lineLimit(10, reservesSpace: true)
.onChange(of: viewModel.description) {
viewModel.validateDescriptionLength()
}
} header: {
Text(String.descriptionString)
} footer: {
VStack(alignment: .leading) {
Text(String.shopDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength))
.font(.uiFootnote)
Text(String.shopDescriptionIsInvalid)
.font(.uiFootnote)
.foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear)
}
}

Section {
Expand Down
32 changes: 31 additions & 1 deletion NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,37 @@ final class ShopCreateViewModel {
}

var hasInvalidData: Bool {
Utility.isBlank(name)
hasInvalidDataName || hasInvalidDataDescription
}

var hasInvalidDataName: Bool {
if Utility.isBlank(name) {
return true
}
if name.count > maximumNameLength {
return true
}
return false
}

var hasInvalidDataDescription: Bool {
description.count > maximumDescriptionLength
}

var maximumNameLength: Int {
NativeAppTemplateConstants.maximumShopNameLength
}

var maximumDescriptionLength: Int {
NativeAppTemplateConstants.maximumShopDescriptionLength
}

func validateNameLength() {
name = String(name.prefix(maximumNameLength))
}

func validateDescriptionLength() {
description = String(description.prefix(maximumDescriptionLength))
}

func createShop() {
Expand Down
24 changes: 21 additions & 3 deletions NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,37 @@ private extension ShopBasicSettingsView {
Form {
Section {
TextField(String.shopName, text: $viewModel.name)
.onChange(of: viewModel.name) {
viewModel.validateNameLength()
}
} header: {
Text(String.shopName)
} footer: {
Text(String.shopNameIsRequired)
.font(.uiFootnote)
.foregroundStyle(Utility.isBlank(viewModel.name) ? .validationError : .clear)
VStack(alignment: .leading) {
Text(String.shopNameHelp(maximumLength: viewModel.maximumNameLength))
.font(.uiFootnote)
Text(String.shopNameIsInvalid)
.font(.uiFootnote)
.foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear)
}
}

Section {
TextField(String.descriptionString, text: $viewModel.description, axis: .vertical)
.lineLimit(10, reservesSpace: true)
.onChange(of: viewModel.description) {
viewModel.validateDescriptionLength()
}
} header: {
Text(String.descriptionString)
} footer: {
VStack(alignment: .leading) {
Text(String.shopDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength))
.font(.uiFootnote)
Text(String.shopDescriptionIsInvalid)
.font(.uiFootnote)
.foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear)
}
}

Section {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ final class ShopBasicSettingsViewModel {
}

var hasInvalidData: Bool {
if Utility.isBlank(name) {
if hasInvalidDataName {
return true
}

if hasInvalidDataDescription {
return true
}

Expand All @@ -54,6 +58,36 @@ final class ShopBasicSettingsViewModel {
return false
}

var hasInvalidDataName: Bool {
if Utility.isBlank(name) {
return true
}
if name.count > maximumNameLength {
return true
}
return false
}

var hasInvalidDataDescription: Bool {
description.count > maximumDescriptionLength
}

var maximumNameLength: Int {
NativeAppTemplateConstants.maximumShopNameLength
}

var maximumDescriptionLength: Int {
NativeAppTemplateConstants.maximumShopDescriptionLength
}

func validateNameLength() {
name = String(name.prefix(maximumNameLength))
}

func validateDescriptionLength() {
description = String(description.prefix(maximumDescriptionLength))
}

func reload() {
Task { @MainActor in
isFetching = true
Expand Down
81 changes: 78 additions & 3 deletions NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,91 @@ struct ShopCreateViewModelTest {
#expect(viewModel.isCreating == false)
}

@Test("Has invalid data", arguments: ["", "Shop Name 1"])
func hasInvalidData(name: String) {
@Test
func maximumNameLength() {
let viewModel = ShopCreateViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
messageBus: messageBus
)

#expect(viewModel.maximumNameLength == 100)
}

@Test
func maximumDescriptionLength() {
let viewModel = ShopCreateViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
messageBus: messageBus
)

#expect(viewModel.maximumDescriptionLength == 1_000)
}

@Test("Name validation", arguments: [
("", true), // blank → invalid
("a", false), // 1 char → valid
("Shop Name 1", false), // normal → valid
(String(repeating: "a", count: 100), false), // exactly 100 → valid
(String(repeating: "a", count: 101), true) // 101 → invalid
])
func nameValidation(name: String, shouldBeInvalid: Bool) {
let viewModel = ShopCreateViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
messageBus: messageBus
)

viewModel.name = name
#expect(viewModel.hasInvalidData == (name == "" ? true : false))

#expect(viewModel.hasInvalidDataName == shouldBeInvalid)
}

@Test("Description validation", arguments: [
("", false), // empty → valid
("Short note.", false), // short → valid
(String(repeating: "x", count: 1000), false), // exactly 1000 → valid
(String(repeating: "x", count: 1001), true) // 1001 → invalid
])
func descriptionValidation(description: String, shouldBeInvalid: Bool) {
let viewModel = ShopCreateViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
messageBus: messageBus
)

viewModel.description = description

#expect(viewModel.hasInvalidDataDescription == shouldBeInvalid)
}

@Test
func validateNameLengthTruncatesCorrectly() {
let viewModel = ShopCreateViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
messageBus: messageBus
)

viewModel.name = String(repeating: "a", count: 100) + "EXTRA"
viewModel.validateNameLength()

#expect(viewModel.name == String(repeating: "a", count: 100))
}

@Test
func validateDescriptionLengthTruncatesCorrectly() {
let viewModel = ShopCreateViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
messageBus: messageBus
)

viewModel.description = String(repeating: "x", count: 1500)
viewModel.validateDescriptionLength()

#expect(viewModel.description.count == 1_000)
}

@Test
Expand Down
Loading
Loading