I am trying to change every duplicate in an array to the same string but with a number of this element in a row at the end. For example change ["text", "word", "text", "text"] into ["text1", "word", "text2", "text3"].
Here are my ideas but they still don't work.
struct Test: View {
#State var array = ["text", "słowo", "text", "text", "siema", "elo", "siema"]
#State var test = [String : [Int]]()
var body: some View {
VStack{
ForEach(array, id:\.self) { i in
Text(i)
}
}.onAppear {
for (index,dateString) in array.enumerated() {
if(test[dateString] == nil){
test[dateString] = [index]
}else{
test[dateString]?.append(index)
}
}
test = test.filter { key, value in
value.count > 1
}
print(test)
var num = 1
for i in array.indices {
if test.keys.contains(array[i]) {
array[i] = array[i] + "\(num)"
num += 1
}
}
print(array)
}
}
}
Please help!!!
This is a straight Swift problem, not SwiftUI. Don't confuse the issue by trying to do that in your View code. This is pretty simple Swift to write:
let array = ["one", "even", "three", "even", "five", "even"]
func replaceInstances(of aString: String, with newString: String, inStringArray stringArray: [String]) -> [String] {
var output = [String]()
var itemCount = 0
for item in stringArray {
if item == aString {
itemCount += 1
output.append("\(newString)\(itemCount)")
} else {
output.append(item)
}
}
return output
}
print(replaceInstances(of: "even", with: "newString", inStringArray: array))
That outputs
["one", "newString1", "three", "newString2", "five", "newString3"]
Or
outputs:
["text1", "słowo", "text2", "text3", "siema", "elo", "siema"]
Let's use one of Swift's awesome superpowers, extensions to the rescue!
Here we teach Swift's array of String a trick to de-duplicate its contents whilst maintaining the order of the elements.
extension Array where Element == String {
var deduplicated : Self {
var occurrences = [String : Int]()
return self.map { item in
guard let count = occurrences[item]
else {
occurrences[item] = 1 // first encounter
return item // unmodified
}
occurrences[item] = count + 1
return "\( item )\( count )"
}
}
}
A quick test...
var array = ["text", "słowo", "text", "text", "siema", "elo", "siema"]
print(array.deduplicated)
// prints: ["text", "słowo", "text1", "text2", "siema", "elo", "siema1"]
The above extension can be hidden away in a sub-folder in your project called "Exntesions" as usual in a file called "Array.swift" as usual.
Now in your view you just need to make this one small change
ForEach(array.deduplicated, id:\.self) { i in
Text(i)
}
Related
Model:
enum TaskType: Int, Codable {
case upcoming = 0
case inProgress
case testing
case completed
var title: String {
switch self {
case .upcoming:
return "Upcoming"
case .inProgress:
return "In Progress"
case .testing:
return "Testing"
case .completed:
return "Completed"
}
}
}
struct TasksModel: Encodable, Decodable {
var upcomingArray: [TaskInfo]
var inProgressArray: [TaskInfo]
var testingArray: [TaskInfo]
var completedArray: [TaskInfo]
}
struct TaskInfo: Codable, Equatable, Identifiable {
var id: String
var title: String
var description: String
var taskStatus: TaskType
var taskDate = Date()
}
VM:
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel
self.tasksArray = TasksModel.init(upcomingArray: [], inProgressArray: [], testingArray: [], completedArray: [])
}
So now that I could locate the record with received taskID and change the taskStatus, I need also to move the record from upcomingArray to inProgressArray. This is what I’m trying:
func inProgressSetTask(taskID: String) {
#StateObject var viewModel = HomeVM()
if let index = viewModel.tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
// Update task status
viewModel.tasksArray.upcomingArray[index].taskStatus = .inProgress
// Need to remove from upcomingArray and append into inProgressArray
viewModel.tasksArray.upcomingArray.remove(at: index)
var lastIndex = viewModel.tasksArray.inProgressArray.last
viewModel.tasksArray.inProgressArray[lastIndex].append()
viewModel.save()
// End
} else {
…
Updating taskStatus above working fine but remove from one array into another is not.
This code above will repeat for each array after else. Appreciate any help.
you could try the following example code to achieve what you want:
(note, you should have #StateObject var viewModel = HomeVM() outside of the func inProgressSetTask(taskID: String) {...}
or pass it in as a parameter)
EDIT-1: moving the function with all arrays into HomeVM and assuming id are unique.
func inProgressSetTask(taskID: String) {
print("InProgress Set ID# \(taskID)")
// with index, using `firstIndex`
if let index = viewModel.tasksArray.inProgressArray.firstIndex(where: {$0.id == taskID}) {
// do something with the index
viewModel.tasksArray.inProgressArray[index].title = "xxx"
}
// with TaskInfo, using `first`
if var taskInfo = viewModel.tasksArray.inProgressArray.first(where: {$0.id == taskID}) {
// do something with the taskInfo
taskInfo.title = "xxx"
}
}
With all arrays of TaskInfo, use the function setTaskFromAll(...) in HomeVM. For example: viewModel.setTaskFromAll(taskID: "1")
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel = TasksModel.init(upcomingArray: [], inProgressArray: [], testingArray: [], completedArray: [])
func setTaskFromAll(taskID: String) {
if let index = tasksArray.inProgressArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.inProgressArray[index].title = "inProgress"
} else {
if let index = tasksArray.completedArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.completedArray[index].title = "completed"
} else {
if let index = tasksArray.testingArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.testingArray[index].title = "testing"
} else {
if let index = tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.upcomingArray[index].title = "upcoming"
}
}
}
}
}
}
EDIT-2:
However, since you already have the "TaskType" of each array in the TaskInfo struct, why not remove TasksModel
and use a single array of TaskInfo in your HomeVM. Like this:
class HomeVM: ObservableObject {
#Published var tasksArray: [TaskInfo] = [
TaskInfo(id: "1", title: "title1", description: "description1", taskStatus: .upcoming),
TaskInfo(id: "2", title: "title2", description: "description2", taskStatus: .inProgress)
// ....
]
func setTask(taskID: String, to taskType: TaskType) {
if let index = tasksArray.firstIndex(where: {$0.id == taskID}) {
tasksArray[index].taskStatus = taskType
}
}
func getAllTaskInfo(_ oftype: TaskType) -> [TaskInfo] {
return tasksArray.filter{$0.taskStatus == oftype}
}
}
and use it like this: viewModel.setTask(taskID: "1", to: .testing) and viewModel.getAllTaskInfo(.inProgress)
EDIT-3: to remove from one array and append to another, using your TasksModel scheme, use this:
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel = TasksModel(upcomingArray: [
TaskInfo(id: "1", title: "title1", description: "description1", taskStatus: .upcoming),
TaskInfo(id: "2", title: "title2", description: "description2", taskStatus: .upcoming)
], inProgressArray: [
TaskInfo(id: "3", title: "title3", description: "description3", taskStatus: .inProgress),
TaskInfo(id: "4", title: "title4", description: "description4", taskStatus: .inProgress)
], testingArray: [], completedArray: [])
func inProgressSetTask(taskID: String) {
if let index = tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
// update task status
tasksArray.upcomingArray[index].taskStatus = .inProgress
// get the upcomingArray taskInfo
let taskInfo = tasksArray.upcomingArray[index]
// remove from upcomingArray
tasksArray.upcomingArray.remove(at: index)
// append to inProgressArray
tasksArray.inProgressArray.append(taskInfo)
} else {
// ...
}
}
}
Use it like this: viewModel.inProgressSetTask(taskID: "1")
As you can plainly see, you are much better-off with the EDIT-2, you are repeating/duplicating things in EDIT-3 for no reason. There is no need for separate arrays for the different TaskType, you already have this info in the TaskInfo var taskStatus: TaskType. With EDIT-2, use viewModel.getAllTaskInfo(.inProgress) to get all TaskInfo of a particular type, just like it would be if you used a separate array.
You are attempting to compare a String to a TaskInfo, because the elements of an inProgressArray are of type TaskInfo. What you need to do is drill into the array and get to the .id. That is simple to do. In the .first(where:), you simply pass a closure of $0.id == taskID.
if let index = viewModel.tasksArray.inProgressArray.first(where: { $0.id == taskID } ) {
for some reason I cannot compare 2 different arrays, for example:
if (self.data.contains(where: self.compareData)){
}
throws:
Cannot convert value of type '[dataModel]' to expected argument type
'(dataModel) throws -> Bool'
and trying to compare them using == also throws an error as Binary operator '==' cannot be applied to operands of type '((dataModel) throws -> Bool) throws -> Bool' and '[dataModel]'
the arrays are formatted as:
var data : [dataModel] = [dataModel] ()
var compareData : [dataModel] = [dataModel]()
I'm checking to see if that array is set to the same array that was sent before replacing data and updating a table.
EDIT: here is the dataModels code:
class dataModel {
var teamName : String = ""
var aboutTeams : String = ""
// var rate : String = "5" // team Rating
var messageID : String = ""
}
You can compare properties of elements within a dataModel if they are inherently comparable/equatable, like String or Int. If you want to be able to test equality for specific elements within the dataModel array itself, then you will have to make your dataModel conform to Equatable.
I believe the problem you are facing has to do with making your dataModel class or struct conform to Equatable, or with how you are populating data and compareData.
This basic example should give you an idea of how to do it. It tests equality for different portions of the data (smaller and larger entities):
struct dataModel: Equatable {
let id: Int
let name: String
}
var data : [dataModel] = [dataModel]()
var compareData : [dataModel] = [dataModel]()
data = [
dataModel(id: 5, name: "five"),
dataModel(id: 3, name: "three")
]
compareData = [
dataModel(id: 5, name: "five"),
dataModel(id: 2, name: "three")
]
if data[0].id == compareData[0].id {
print("First item ID is equal")
}
if data[0].name == compareData[0].name {
print("First item name is equal")
}
if data[0] == compareData[0] {
print("First item of both data and compareData are equal")
}
if data == compareData {
print("Data Arrays are equal")
} else {
print("Data Arrays are not equal")
}
I saw that you just showed the code for your DataModel (class should usually be capitalized) so here's a more customized example using your actual implementation of the class:
class DataModel: Equatable {
static func == (lhs: DataModel, rhs: DataModel) -> Bool {
guard lhs.aboutTeams == rhs.aboutTeams else {return false}
guard lhs.messageID == rhs.messageID else {return false}
guard lhs.teamName == rhs.teamName else {return false}
return true
}
var teamName : String = ""
var aboutTeams : String = ""
var messageID : String = ""
}
var data : [DataModel] = [DataModel]()
var compareData : [DataModel] = [DataModel]()
data = [
DataModel(),
DataModel()
]
data[0].teamName = "raptors"
data[0].messageID = "5"
compareData = [
DataModel(),
DataModel()
]
compareData[0].teamName = "raptors"
compareData[0].messageID = "4"
if data[0].teamName == compareData[0].teamName {
print("Element 0 team names are equal")
} else {
print("Element 0 team names are not equal")
}
if data[0].messageID == compareData[0].messageID {
print("Elemeant 0 message IDs are equal")
} else {
print("Elemeant 0 message IDs are not equal")
}
if data[1] == compareData[1] {
print("Element 1 of data and compareData are equal")
}
if data == compareData {
print("Arrays are equal")
} else {
print("Arrays are not equal")
}
The console output is:
Element 0 team names are equal
Elemeant 0 message IDs are not equal
Element 1 of data and compareData are equal
Arrays are not equal
Hope this helps!
That's because that function you are trying to use requires an NSPredicate.
One way to check if an array contains a subset of another array would be to do the following:
var dataSet = Set(data)
var compareDataSet = Set(compareData)
if compareDataSet.isSubsetOf(dataSet) {
// Do something
}
EDIT: Your class needs to conform to equatable
class dataModel: Equatable {
var teamName : String = ""
var aboutTeams : String = ""
// var rate : String = "5" // team Rating
var messageID : String = ""
override func isEqual(_ object: Any?) -> Bool {
if let object = object as? dateModel {
return self.teamName == object.teamName && self.aboutTeams == object.aboutTeams && self.messageID == object.messageID
} else {
return false
}
}
}
You can try with this example.
import UIKit
class ViewController: UIViewController, NavDelegate {
var data : [dataModel] = [dataModel]()
var compareData : [dataModel] = [dataModel]()
override func viewDidLoad() {
super.viewDidLoad()
data = [
dataModel(teamName: "cyber1", aboutTeams: "strong", messageID: "1001"),
dataModel(teamName: "cyber1", aboutTeams: "good", messageID: "1002")
]
compareData = [
dataModel(teamName: "cyber1", aboutTeams: "strong", messageID: "1001"),
dataModel(teamName: "cyber1", aboutTeams: "good 2", messageID: "1002")
]
let dataSet = Set(data)
let compareDataSet = Set(compareData)
if compareDataSet == dataSet {
print("compareDataSet and dataSet are equal")
}else{
print("compareDataSet and dataSet are not equal")
}
}
struct dataModel: Hashable {
var teamName : String = ""
var aboutTeams : String = ""
var messageID : String = ""
}
}
If all you are requiring is to see if two arrays are equal, you can do it with a simple function:
func compare<T: Equatable>(_ array1: [T], _ array2: [T] ) -> Bool {
guard array1.count == array2.count else {return false}
for i in 0 ..< array1.count {
if array1[i] != array2[i] { return false}
}
return true
}
You could even build this as an extension of Array
extension Array where Element: Equatable{
func compare(to array: [Element]) -> Bool {
guard array.count == self.count else {return false}
for i in 0 ..< array.count {
if array[i] != self[i] { return false}
}
return true
}
}
The compiler won't let you compare two arrays of different types.
I am using the following array structure to create a tableView with sections
struct words {
var sectionName : String!
var coptic : [String]!
var english : [String]!
}
var array = [words]()
var filtered = [words]()
array = [words(sectionName: "", coptic: [""], English: [""])]
I want to utilize a searchcontroller using similar code to this
func updateSearchResults(for searchController: UISearchController) {
// If we haven't typed anything into the search bar then do not filter the results
if searchController.searchBar.text! == "" {
filtered = array
} else {
// Filter the results
filtered = array.filter { $0.coptic.lowercased().contains(searchController.searchBar.text!.lowercased()) }
}
Unfortunately, because coptic is a [String], and not simply a String, the code doesn't work. Is there a way to modify this to be able to filter a search for the coptic subsection?
you can do like this.
func updateSearchResults(for searchController: UISearchController) {
// If we haven't typed anything into the search bar then do not filter the results
if searchController.searchBar.text! == ""
{
filtered = array
}
else
{
filtered.removeAll()
array.forEach({ (word:words) in
var tempWord:words = words.init(sectionName: word.sectionName, coptic: [""], english: [""])
let copticArray = word.coptic.filter({ (subItem:String) -> Bool in
let a = subItem.lowercased().contains(searchController.searchBar.text!.lowercased())
return a;
})
tempWord.coptic = copticArray
filtered.append(tempWord)
})
}
}
Input array = array = [words(sectionName: "abc", coptic: ["apple","ball","cat","dog"], english: [""])]
Search For "app"
OUTPUT words(sectionName: abc, coptic: ["apple"], english: [""])]
Please refer the following code:
import UIKit
struct Item {
var brandId = 1
var name: String = ""
}
struct Store {
var areaName = ""
var name: String = ""
}
let itemArray = [Item(brandId: 1, name: "item1"), Item(brandId: 2, name: "item2"), Item(brandId: 1, name: "item3") ]
let storeArray = [Store(areaName: "hk", name: "store1"), Store(areaName: "bj", name: "store2"), Store(areaName: "hk", name: "store3")]
var intKeys = [Int]()
var groupedItems = [[Item]]()
var stringKeys = [String]()
var groupedStores = [[Store]]()
extension Array {
func transTo2d() -> [[Element]] {
let grouped = [[Element]]()
return grouped
}
}
itemArray.forEach { (item) in
let brandId = item.brandId
if !intKeys.contains(brandId) {
intKeys.append(brandId)
var newArray = [Item]()
newArray.append(item)
groupedItems.append(newArray)
} else {
let index = intKeys.index(of: brandId)!
groupedItems[index].append(item)
}
}
My final goal is could using itemArray.transTo2d() get a 2d array based on item's brandId, using storeArray.transTo2d() get a 2d array based on store's areaName. I don't how to generic the function that trans 1d array to a 2d array based on the key?
I don't think you can write a generic extension for an Array where the elements will either be of type Item or Store since both of them don't share any relation for you to write a common generic method. You can write extensions for Array where the elements will be of the mentioned type. You just need to conform both of your structs to the equatable protocol.
struct Item {
var brandId = 1
var name: String = ""
}
extension Item : Equatable{
static func ==(lhs: Item, rhs: Item) -> Bool{
return lhs.brandId == rhs.brandId
}
}
struct Store {
var areaName = ""
var name: String = ""
}
extension Store : Equatable{
static func ==(lhs: Store, rhs: Store) -> Bool{
return lhs.areaName == rhs.areaName
}
}
extension Array where Element == Store{
func transform()->[[Store]]{
var storeArray = self
var groupedArray = [[Store]]()
while storeArray.count > 0{
if let firstElement = storeArray.first{
groupedArray.append(storeArray.filter{$0.areaName == firstElement.areaName})
storeArray = storeArray.filter{$0.areaName != firstElement.areaName}
}
}
return groupedArray
}
}
extension Array where Element == Item{
func transform()->[[Item]]{
var itemArray = self
var groupedArray = [[Item]]()
while itemArray.count > 0{
if let firstElement = itemArray.first{
groupedArray.append(itemArray.filter{$0.brandId == firstElement.brandId})
itemArray = itemArray.filter{$0.brandId != firstElement.brandId}
}
}
return groupedArray
}
}
Using the transform function
let storeArray = [Store(areaName: "hk", name: "store1"), Store(areaName: "bj", name: "store2"), Store(areaName: "hk", name: "store3")]
let itemArray = [Item(brandId: 1, name: "item1"), Item(brandId: 2, name: "item2"), Item(brandId: 1, name: "item3") ]
print(storeArray.transform())
print(itemArray.transform())
This will print this output which is what I believe you wanted.
[[Store(areaName: "hk", name: "store1"), Store(areaName: "hk", name: "store3")], [Store(areaName: "bj", name: "store2")]]
[[Item(brandId: 1, name: "item1"), Item(brandId: 1, name: "item3")], [Item(brandId: 2, name: "item2")]]
I have an array of object.
I want to get distinct elements in this array by comparing objects based on its name property
class Item {
var name: String
init(name: String) {
self.name = name
}
}
let items = [Item(name:"1"), Item(name:"2"), Item(name:"1"), Item(name:"1"),Item(name:"3"), Item(name:"4")]
result:
let items = [Item(name:"1"), Item(name:"2"),Item(name:"3"), Item(name:"4")]
how can I do this in swift?
Here is an Array extension to return the unique list of objects based on a given key:
extension Array {
func unique<T:Hashable>(by: ((Element) -> (T))) -> [Element] {
var set = Set<T>() //the unique list kept in a Set for fast retrieval
var arrayOrdered = [Element]() //keeping the unique list of elements but ordered
for value in self {
if !set.contains(by(value)) {
set.insert(by(value))
arrayOrdered.append(value)
}
}
return arrayOrdered
}
}
For your example you can do:
let uniqueBasedOnName = items.unique{$0.name}
Hope this will help you:
class Item:Equatable, Hashable {
var name: String
init(name: String) {
self.name = name
}
var hashValue: Int{
return name.hashValue
}
}
func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.name == rhs.name
}
let items = [Item(name:"1"), Item(name:"2"), Item(name:"1"), Item(name:"1"),Item(name:"3"), Item(name:"4")]
var uniqueArray = Array(Set(items))
In Swift you can use Equatable protocol to distinguish unique element in an Array of object.
struct Item:Equatable{
var name:String
var price:Double
init(name:String,price:Double) {
self.name = name
self.price = price
}
static func ==(lhs: Item, rhs: Item) -> Bool{
return lhs.name == rhs.name
}
}
class ViewController: UIViewController {
var books = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
items.append(Item(name: "Example 1", price: 250.0))
items.append(Item(name: "Example 2", price: 150.0))
items.append(Item(name: "Example 1", price: 150.0))
items.append(Item(name: "Example 1", price: 150.0))
items.append(Item(name: "Example 3", price: 100.0))
items.unique().forEach { (item) in
print(item.name)
}
}
}
extension Sequence where Iterator.Element: Equatable {
func unique() -> [Iterator.Element] {
return reduce([], { collection, element in collection.contains(element) ? collection : collection + [element] })
}
}
I used the sweet answer of #Ciprian Rarau and then realised I don't even need to add the elements in the first place if they are not unique. So I wrote a little extension for that (inspired by the answer).
extension Array {
public mutating func appendIfUnique<T: Hashable>(_ newElement: Element, check property: ((Element) -> (T))) {
for element in self {
if property(element) == property(newElement) { return }
}
append(newElement)
}
}
Append elements only if unique:
array.appendIfUnique(newElement, check: { $0.name })
Solution that uses keypaths instead of closures:
extension Sequence {
func uniqued<Type: Hashable>(by keyPath: KeyPath<Element, Type>) -> [Element] {
var set = Set<Type>()
return filter { set.insert($0[keyPath: keyPath]).inserted }
}
}
Example for struct Mock { var uuid: UUID; var num: Int }
let uuid = UUID()
let arr = [
Mock(uuid: uuid, num: 1),
Mock(uuid: uuid, num: 2),
Mock(uuid: UUID(), num: 3)
]
let unique = arr.uniqued(by: \.uuid)
unique array will contain first (num = 1) and last (num = 3) elements.