diff --git a/README.md b/README.md index 14895eae..793990a6 100644 --- a/README.md +++ b/README.md @@ -1 +1,149 @@ -# ios-contact-manager-ui \ No newline at end of file +# ๐Ÿ—“ README +#### โœจ iyeah & ๐Ÿ Jenna
+##### 2023๋…„ 1์›” 30์ผ โ†’ 2023๋…„ 2์›” 10์ผ + +ใ€€ + +# Step 1๏ธโƒฃ +## \# ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> ๊ธฐ์กด ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉด์„œ, ํ”„๋กœ์ ํŠธ ๋ฐ ํด๋”๋ช…์„ ๋ฐ”๊ฟ”์•ผ ํ•  ์ง€ ๊ณ ๋ฏผ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +> ํƒ€๊ฒŸ `ContactManagerUI`์„ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•œ๋‹ค๋Š” ๊ฒƒ์€ ํ”„๋กœ์ ํŠธ์— ๋ณ„๋„์˜ ์ œํ’ˆ์„ ๋งŒ๋“ ๋‹ค๋Š” ๊ฒƒ์ด๋ฏ€๋กœ, +์˜คํžˆ๋ ค ์ด๋ฆ„์„ ๊ฐ™๊ฒŒ ๋งž์ถ”๋ ค ํ•  ํ•„์š”๊ฐ€ ์—†์Œ์„ ๊นจ๋‹ฌ์•„ ํ”„๋กœ์ ํŠธ ๋ฐ ํด๋”๋ช… ์œ ์ง€ + + +## \# ํ•™์Šต๋‚ด์šฉ ์š”์•ฝ +>`Target` +ํ•˜๋‚˜์˜ ํƒ€๊ฒŸ์€ ํ•˜๋‚˜์˜ ํ”„๋กœ๋•ํŠธ์ด๋ฉฐ, +ํ”„๋กœ์ ํŠธ ๋‚ด์— ์—ฌ๋Ÿฌ ๊ฐœ์˜(= ๋ณ„๊ฐœ์˜) ํƒ€๊ฒŸ(ํ”„๋กœ๋•ํŠธ)์ด ์กด์žฌํ•  ์ˆ˜ ์žˆ๋‹ค. + + + +ใ€€ +ใ€€ใ€€ +
+# Step 2๏ธโƒฃ +## \# ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ Model์ธ `ContactManageSystem`์— ๋„ฃ์„์ง€, `ViewController`์— ๊ตฌํ˜„ ํ•ด์•ผํ•  ์ง€์— ๋Œ€ํ•œ ๊ณ ๋ฏผ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +
+๋ฆฌ๋ทฐ์–ด ์˜๊ฒฌ ๐Ÿถ +๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ž์ฒด๋ฅผ ์ƒ์ˆ˜์ฒ˜๋Ÿผ ๋งŒ๋“ค์–ด์„œ ViewController์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒŒ ์ €๋Š” ๋” ๊น”๋”ํ•œ ๊ฒƒ ๊ฐ™์•„์š”! ์ƒ์ˆ˜์ฒ˜๋Ÿผ ์“ฐ๊ณ  ๋‚˜์ค‘์— ์ œ๊ฑฐํ•˜๋ฉด ๋˜๋‹ˆ๊นŒ์š”! +
+ +โ†’ `ViewController`์— `dummyData`๋ฅผ ์ƒ์ˆ˜๋กœ ์„ ์–ธํ•˜์—ฌ ํ•ด๊ฒฐ + +ใ€€ +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> MVC ํด๋”์— ๋„ฃ๊ธฐ ์• ๋งคํ•œ ํŒŒ์ผ๋“ค ์ฒ˜๋ฆฌ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +
+๋ฆฌ๋ทฐ์–ด ์˜๊ฒฌ ๐Ÿถ +์ด ๋ถ€๋ถ„์€ ์ •ํ•ด์ ธ์žˆ๋Š” ๋‹ต์€ ์—†์œผ๋‹ˆ, ํŒ€ ๋‚ด์—์„œ ์•ฝ์†ํ•ด์„œ ํด๋”๋กœ ์ •๋ฆฌํ•ด๋„ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค.
MVC ํด๋” ๋ฐ–์œผ๋กœ ๋นผ๋‘๋Š” ๊ฒƒ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.
LaunchScreen์„ ๋”ฐ๋กœ ํด๋”๋กœ ๋งŒ๋“œ๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ณ , AppConfiguration๊ณผ ๊ฐ™์ด ํด๋”๋ฅผ ๋งŒ๋“ค์–ด์„œ AppDelegate, SceneDelegate ํŒŒ์ผ์„ ๋„ฃ๊ธฐ๋„ํ•ฉ๋‹ˆ๋‹ค.
์ œ๊ฐ€ ๋ง์”€๋“œ๋ฆฐ ๋ถ€๋ถ„์€ ์ฐธ๊ณ ๋งŒํ•˜์‹œ๊ณ  ํŒ€์›๊ณผ ๊ฐ™์ด ์ด์•ผ๊ธฐํ•ด๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค:) +
+ +โ†’ ์•„๋ž˜์™€ ๊ฐ™์ด ํด๋”๋ง ์ง„ํ–‰ + +![](https://i.imgur.com/cUjN62R.png) + + +ใ€€ +## \# ํ•™์Šต๋‚ด์šฉ ์š”์•ฝ +> 1. ๋”๋ฏธ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ์›ํ•˜๋Š” ์‹œ์ ์— ์‚ญ์ œ๊ฐ€ ํŽธํ•˜๋„๋ก ์ƒ์ˆ˜๋กœ ๊ตฌํ˜„ +> 2. ํด๋”๋ง์€ ์ •ํ•ด์ง„ ๊ฒŒ ์—†๊ธฐ ๋•Œ๋ฌธ์— ํŒ€์›๊ณผ ์ƒ์˜ํ•ด์„œ ์ •ํ•˜๊ธฐ + + + +
+ +# Step 3๏ธโƒฃ +## \# ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> `.phonePad`๋Š” -์ž…๋ ฅ์„ ์ง€์›ํ•˜์ง€ ์•Š์Œ +โ†’ ์•„์ดํฐ ๊ธฐ๋ณธ์•ฑ์ฒ˜๋Ÿผ, ํ…์ŠคํŠธํ•„๋“œ์— ๊ฐ’์ด ์ž…๋ ฅ๋  ๋•Œ๋งˆ๋‹ค `-`๊ฐ€ ์ ์ ˆํ•œ ์œ„์น˜์— ์‚ฝ์ž…๋˜๊ฒŒ๋” ์ž๋™๋ณ€ํ™˜ ํ•ด์ฃผ๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€๋กœ ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์ƒ๊น€ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +> `textField(_:shouldChangeCharactersIn:replacementString:)`๋ฉ”์„œ๋“œ + +- `AddProfileViewController๊ฐ€` `UITextFieldDelegate`ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒ +- `textField(_:shouldChangeCharactersIn:replacementString:)`๋ฉ”์„œ๋“œ๋กœ ์ƒˆ๋กœ์šด ์ž…๋ ฅ์˜ ์ข…๋ฅ˜์— ๋”ฐ๋ผ + - ์—ฐ๋ฝ์ฒ˜ ํ…์ŠคํŠธํ•„๋“œ์—์„œ์˜ ์ž…๋ ฅ์„ ํ—ˆ์šฉํ•˜๊ฑฐ๋‚˜, + - ๋˜๋Š” ์–‘์‹์— ๋งž์ถ˜ ๋ณ€ํ™˜๊ฐ’์œผ๋กœ ๋Œ€์ฒดํ•˜์—ฌ ์ง์ ‘ ํ• ๋‹น(์ž…๋ ฅ ๊ฑฐ์ ˆ) +- `PhoneNumberRegularExpressions`์—ด๊ฑฐํ˜•์œผ๋กœ ์ž๋ฆฟ์ˆ˜๋ณ„ ๋ณ€ํ™˜ ๋ฐฉ์‹์„ ์ •์˜ + +ใ€€ +## \# ํ•™์Šต๋‚ด์šฉ ์š”์•ฝ +> Delegate์˜ ์ •์˜๋Š” `์œ„์ž„ํ•˜๋‹ค`, +๊ฐœ์ธ์ ์œผ๋กœ Delegate Pattern์ด๋ž€ `์ฑ…์ž„์ž-๋Œ€๋ฆฌ์ž ํŒจํ„ด` ์ด๋ผ๊ณ  ์ดํ•ด +#### ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•œ ๋ถ€๋ถ„ โ–ผ +- `AddProfileViewController`(์ดํ•˜ AddVC)๋Š” **์ฑ…์ž„์ž** +- `ListProfileViewController`(์ดํ•˜ ListVC)๋Š” `AddProfileViewControllerDelegate`(์ดํ•˜ AddVCDelegate) ์ž๊ฒฉ์ฆ์„ ๊ฐ€์ง +- `ListVC`๋Š” ์ƒˆ๋กœ์šด ๋ทฐ(`AddV`)๋ฅผ ์˜ฌ๋ฆด ๋•Œ ๋ณธ์ธ(self)์„ ๊ทธ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ(`AddVC`)์˜ **๋Œ€๋ฆฌ์ž**(Delegate)๋กœ ์ง€์ •ํ•˜์—ฌ ํ•จ๊ป˜ ๋ณด๋‚ด์ง€๊ณ , + ```Swift + AddVC.delegate: AddVCDelegate = self //self: ListVC(AddVCDelegate๋กœ์„œ์˜ ListVC) + ``` +- ๋Œ€๋ฆฌ์ž๋Š” ์ฑ…์ž„์ž(AddVC) ๋‚ด์— ๋จธ๋ฌผ๋ฉฐ(= .delegate๋ณ€์ˆ˜์— ํ• ๋‹น๋œ ์ฑ„) +๋Œ€๋ฆฌ์ž๋กœ์„œ ์š”๊ตฌ๋ฐ›์€ ๋™์ž‘(ํ”„๋กœํ† ์ฝœ ํ•„์ˆ˜๊ตฌํ˜„ ๋ฉ”์„œ๋“œ)์„ ์ ์ ˆํ•œ ์‹œ์ ์— ์ˆ˜ํ–‰ + ```Swift + // ๊ทธ ๋™์ž‘์€ AddProfileViewController์—์„œ 'Save๋ฒ„ํŠผ์ด ๋ˆŒ๋ €์„ ๋•Œ' ํ˜ธ์ถœ๋˜์–ด, + // (๊ฒ€์ฆ ์™„๋ฃŒ๋œ) ์ƒˆ๋กœ์šด ์ด๋ฆ„ยท๋‚˜์ดยท์—ฐ๋ฝ์ฒ˜ ์ •๋ณด๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ํ”„๋กœํ•„์„ ์ƒ์„ฑ(= ์š”๊ตฌ๋ฐ›์€ ๋™์ž‘) + delegate?.updateProfile(name: name, age: age, tel: tel) + dismiss() + ``` +- `AddV`๊ฐ€ ๋‚ด๋ ค๊ฐ€๋ฉด ๋Œ€๋ฆฌ์ž ์—ญํ• ์„ ๋งˆ๋ฌด๋ฆฌํ•˜๊ณ  ๋Œ์•„์˜จ `ListVC`๋Š” ๊ทธ ๋ฐ์ดํ„ฐ(์ƒˆ ํ”„๋กœํ•„)๋ฅผ ๋ฐ›์•„ ํ•„์š”ํ•œ ์ž‘์—…(profiles์— ์ƒˆ ํ”„๋กœํ•„์„ ๋“ฑ๋ก)์„ ์ด์–ด์„œ ์ˆ˜ํ–‰ + + + +ใ€€ +ใ€€ +
+ +# Step ๐Ÿ…ฑ๐Ÿ…พ๐Ÿ…ฝ๐Ÿ†„๐Ÿ†‚ +## \# ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ํ™”๋ฉด์—์„œ๋„ ์˜ฌ๋ฐ”๋ฅธ ์…€์ด ์‚ญ์ œ๋˜๋„๋ก ํ•˜๊ธฐ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +์‚ผํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ํ™œ์šฉํ•˜์—ฌ, `isSearching`์— ๋”ฐ๋ผ +index๋กœ ์ ‘๊ทผํ•  ํ”„๋กœํ•„ ๋ฐฐ์—ด์ด `profiles` / `filteredProfiles` ์ค‘ ์–ด๋А์ชฝ์ธ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ +```Swift +let profile = isSearching ? profileSearchResults[indexPath.row] : profiles[indexPath.row] +``` + +ใ€€ +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> ๋™๋ช…์ด์ธ์ด ์žˆ์–ด๋„ ์ •ํ™•ํžˆ ์‚ญ์ œ๋˜๋„๋ก ํ•˜๊ธฐ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +1. ์ดˆ๋ฐ˜์— profiles์—์„œ `name`์ด ์ผ์น˜ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๊ณ  ํ–ˆ์ง€๋งŒ, ๋™๋ช…์ด์ธ์ด ๋Œ€์‹  ์‚ญ์ œ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ +2. Model์ธ `Profile`์ด Hashableํ”„๋กœํ† ์ฝœ(= ์ฆ‰ Equatable๋„ ์ฑ„ํƒํ•จ)์„ ์ฑ„ํƒํ–ˆ์œผ๋ฏ€๋กœ ์ปค์Šคํ…€ ์ดํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๋ ค๋‹ค๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐœ์ƒ์ด ๋– ์˜ฌ๋ผ ๋ณด๋ฅ˜ +3. `tableView(_:cellForRowAt:)`๋ฉ”์„œ๋“œ์—์„œ `indexPath.row`๋กœ `profile`์„ ๋ถˆ๋Ÿฌ์™”์œผ๋ฏ€๋กœ, ์—ญ์œผ๋กœ ํ•ด๋‹น index์˜ profile์„ ๊บผ๋‚ด์–ด ์‚ญ์ œํ•˜๋ฉด ํ•ด๋‹น ์…€์˜ profile์ด ์‚ญ์ œ๋  ๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ๊ตฌํ˜„ + +ใ€€ +### ๐Ÿ‘ฟ ํŠธ๋Ÿฌ๋ธ” +> ์ด๋ฆ„์— ๋Œ€์†Œ๋ฌธ์ž๊ฐ€ ์„ž์—ฌ ์žˆ์„ ๋•Œ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •๋ ฌ๋˜์ง€ ์•Š๊ณ  ๊ฒ€์ƒ‰์ด ๋˜์ง€ ์•Š์Œ + +### ๐Ÿ˜ˆ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• +์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ ์‹œ `lowercased()` ๋ฉ”์„œ๋“œ๋ฅผ ์ ์šฉํ•˜์—ฌ ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„์—†์ด sort๋˜๋„๋ก ํ•จ + +ใ€€ +ใ€€ +## \# ํ•™์Šต๋‚ด์šฉ ์š”์•ฝ +### searchBar ๋งŒ๋“ค๊ธฐ +> **UISearchBar์™€ UISearchController์˜ ์ฐจ์ด** +> - VC.navigationItem.searchController: `UISearchController` +> - UISearchController().searchBar: `UISearchBar` +### ์Šฌ๋ผ์ด๋“œํ•˜์—ฌ ํ•ด๋‹น ์…€ delete ํ•˜๊ธฐ +> `UITableViewDataSource` ํ”„๋กœํ† ์ฝœ ๋‚ด์— ์žˆ๋Š” `tableView(_:commit:forRowAt:)` ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ + + + + + +ใ€€ + ใ€€ diff --git a/ios-cantact-manager/ContactManagerUI/Controllers/ListProfileViewController.swift b/ios-cantact-manager/ContactManagerUI/Controllers/ListProfileViewController.swift index 3151582a..144f8833 100644 --- a/ios-cantact-manager/ContactManagerUI/Controllers/ListProfileViewController.swift +++ b/ios-cantact-manager/ContactManagerUI/Controllers/ListProfileViewController.swift @@ -10,14 +10,27 @@ import UIKit final class ListProfileViewController: UIViewController, AddProfileViewControllerDelegate { private var contactManageSystem = ContactManageSystem() private var profiles: [Profile] { - contactManageSystem.profiles.sorted(by: { $0.name < $1.name }) + contactManageSystem.sortProfiles() } + private lazy var profileSearchResults = [Profile]() + private var isSearching: Bool { + let searchBarController = self.navigationItem.searchController + let isActive = searchBarController?.isActive ?? false + let isEmpty = searchBarController?.searchBar.text?.isEmpty ?? true + return isActive && !isEmpty + } + private let dummyData = [ - Profile(name: "james", age: "30", tel: "010-2222-2222"), - Profile(name: "tom", age: "15", tel: "010-2222-3333"), - Profile(name: "jams", age: "30", tel: "010-2222-2222"), - Profile(name: "toem", age: "15", tel: "010-2222-3333"), - Profile(name: "jamses", age: "30", tel: "010-2222-2222") + Profile(name: "iyeah", age: "1", tel: "010-2222-2222"), + Profile(name: "iyeah", age: "2", tel: "010-2222-3333"), + Profile(name: "iyeah", age: "3", tel: "010-2222-2222"), + Profile(name: "iyeah", age: "4", tel: "010-2222-3333"), + Profile(name: "Jenna", age: "5", tel: "010-2222-3333"), + Profile(name: "Jenna", age: "6", tel: "010-2222-3333"), + Profile(name: "Jenna", age: "7", tel: "010-2222-3333"), + Profile(name: "Jenna", age: "8", tel: "010-2222-3333"), + Profile(name: "iyeah", age: "9", tel: "010-2222-3333"), + Profile(name: "SeSaC", age: "30", tel: "010-2222-2222") ] @IBOutlet private weak var tableView: UITableView! @@ -28,6 +41,7 @@ final class ListProfileViewController: UIViewController, AddProfileViewControlle contactManageSystem.add(profile: $0) } tableView.dataSource = self + makeSearchBar() } @IBAction private func addProfileButtonDidTap(_ sender: UIBarButtonItem) { @@ -47,11 +61,11 @@ final class ListProfileViewController: UIViewController, AddProfileViewControlle extension ListProfileViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - profiles.count + isSearching ? profileSearchResults.count : profiles.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let profile = profiles[indexPath.row] + let profile = isSearching ? profileSearchResults[indexPath.row] : profiles[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileCell", for: indexPath) var content = cell.defaultContentConfiguration() @@ -63,5 +77,32 @@ extension ListProfileViewController: UITableViewDataSource { return cell } + + func tableView(_ tableView: UITableView, + commit editingStyle: UITableViewCell.EditingStyle, + forRowAt indexPath: IndexPath) { + let profile = isSearching ? profileSearchResults[indexPath.row] : profiles[indexPath.row] + contactManageSystem.remove(profile: profile) + tableView.deleteRows(at: [indexPath], with: .fade) + } } +extension ListProfileViewController: UISearchResultsUpdating { + func updateSearchResults(for searchController: UISearchController) { + guard let text = searchController.searchBar.text else { + return + } + profileSearchResults = profiles.filter { + $0.name.lowercased() == text.lowercased() + } + tableView.reloadData() + } + + private func makeSearchBar() { + let searchBar = UISearchController(searchResultsController: nil) + searchBar.searchResultsUpdater = self + searchBar.searchBar.autocapitalizationType = .none + navigationItem.searchController = searchBar + navigationItem.hidesSearchBarWhenScrolling = false + } +} diff --git a/ios-cantact-manager/ios-cantact-manager/ContactManageSystem.swift b/ios-cantact-manager/ios-cantact-manager/ContactManageSystem.swift index 3361ad51..a2f56b86 100644 --- a/ios-cantact-manager/ios-cantact-manager/ContactManageSystem.swift +++ b/ios-cantact-manager/ios-cantact-manager/ContactManageSystem.swift @@ -49,7 +49,7 @@ struct ContactManageSystem { case .listUpProfile: listUpProfile() case .searchProfile: - searchProfile() + break case .stop: stop() } @@ -59,23 +59,19 @@ struct ContactManageSystem { profiles.insert(profile) } + mutating func sortProfiles() -> [Profile] { + profiles.sorted { + let (lhs, rhs) = ($0.name.lowercased(), $1.name.lowercased()) + return lhs != rhs ? lhs < rhs : $0.age < $1.age + } + } + private func listUpProfile() { OutputManager.print(profiles: profiles) } - private func searchProfile() { - do { - OutputManager.print(text: .inputProfileName) - let targetName = try inputManager.targetInput() - let filteredProfileData = profiles.filter { $0.name == targetName } - guard !filteredProfileData.isEmpty else { - OutputManager.printNoMatchingData(name: targetName) - return - } - OutputManager.print(profiles: filteredProfileData) - } catch { - OutputManager.print(text: .invalidInput) - } + mutating func remove(profile: Profile) { + profiles.remove(profile) } mutating func stop() {