swiftPunk
swiftPunk

Reputation: 1

Iterating through set with ForEach in SwiftUI

I am working on some of code that try work with a set with ForEach, it is kind of working, the issue is here that with updating first character of new value it will end updating and does not let to finish the updating process. I want improve the updating logic without using @FocusState for TextField.

My goal is that my codes waits for user to import all new character or the codes understands that the TextField is under editing und user did not press return key on keyboard.

struct ContentView: View {
    
    @State private var set: Set<String> = ["value1", "value2", "value3", "value4"]
    
    var body: some View {
        
        SetforForEachWithBinding(set: $set)
        
    }
}

struct SetforForEachWithBinding: View {
    
    @Binding var set: Set<String>
    
    var body: some View {
        
        List {
            ForEach(set.sorted(by: <), id: \.self) { element in
                
                TextField("Enter value here...", text: Binding(
                    get: { return element },
                    set: { newValue in
                        
                        if set.contains(element) {
                            set.remove(element)
                            set.insert(newValue)
                        }
                        
                    }
                ))
                
            }
        }
    }
}

Upvotes: 2

Views: 7442

Answers (2)

valeCocoa
valeCocoa

Reputation: 344

Disclaimer: this answer is merely a synthesis of what we have discussed already in comments. Keep in mind that you'll really be able to see speed improvements with a number of elements N that is in the magnitude of thousands or hundreds of thousands. I don't think it suits the case of a simple view, but anyway let's cover it for the sake of the discussion.

Your SwiftUI view: ForEach(set.sorted(), id: \.self) { … } has an overall complexity of O(N + N * Log N) where N is the count of the elements stored in the collection to sort (from now on let's establish that N will always refers to that count value). That is cause sorting the collection has a complexity of O(N * Log N) and iterating over all its elements (that's what the ForEach does) has O(N) complexity.

Thus another user has correctly suggested to use a sorted array and keeping it sorted upon addition/removal of elements. This will effectively reduce the ForEach construct to have a O(N) complexity, deferring the O(NLogN) cost to when a mutation of such array is done… But still after such mutation happens, the ForEach will be triggered again cause the state of the view has changed: hence you'll still end up with the same complexity cost. Moreover you'll still have to initially pass a sorted array to the collection, or eventually sort it at initialisation time.

So you could think to improve a bit your original code by using a sorted array, thus leveraging on a NSArray's index(of:inSortedRange:options:usingComparator) when mutating it (the TextField binding):

// assuming elements is the sorted array used to build the ForEach and holding a state in the view
CustomTextFieldView(string: Binding(
                    get: { return element },
                    set: { newValue in
                        elements.remove(element)
                        let i = (elements as NSArray<NSString>)
                            .index(of: newValue, 
                                   inSortedRange: 0..<elements.count, 
                                   options: .insertionIndex, 
                                   usingComparator: { $0.compare($1) }
                        )
                        elements.insert(newValue, at: i)
                    }
                        
                ))

Except… here you're also adding another two O(N) complexity factors in order to remove the old element stored in the array and to insert at the right sort position the new element… Thus your overall complexity for the ForEach now will be: O(3N + N * Log N)). This is worse than using the set as you originally did (Set has amortized O(1) complexity for removal/addition of elements thus it can be taken out from the overall complexity cost calculation).

How can you improve instead the overall complexity cost? By choosing a data structure to hold your elements leveraging on Left Leaning Red-Black Tree.

This data structure keeps its elements sorted upon mutation, that is the complexity for such operation is almost O(Log N).

Thus your whole ForEach (including the mutations) should perform in amortized O(N + Log N).

I've pointed out in the comments an implementation I made of a data structure that leverages on this kind of balanced binary search tree, which is modelled upon an associative array (Dictionary), so it's not a Set per-se, but you could use it in your case to store your elements as the keys and using Void or a Bool as the value to associate to them.

Now of course you'll lose the ability to do specific Set operations (as intersections for example), but as you clearly stated in your question you've chosen a Set because of its O(1) complexity for membership lookup of an element. By adopting instead a left leaning red-black tree you'll lose that sweet O(1) complexity, trading it for O(Log N) on this particular operation, but in the overall complexity of this particular ForEach you'll gain a lower cost.

I repeat: in your case I highly doubt the elements count will ever reach a thousands or hundreds of thousands value so to really notice any speed improvement.

Upvotes: 1

swiftPunk
swiftPunk

Reputation: 1

It turns out need some playing with code like this way, if you know better way I will accept your answer. thanks.

PS: For using Set in ForEach and unleashing full power of Set, we can use an identifiable type for elements for our Set, then using id: \.id, after that we can have multiple elements with same string and deferent id's also the power of Set.

struct ContentView: View {
    
    @State private var set: Set<String> = ["value1", "value2", "value3", "value4"]
    
    var body: some View {
        
        SetforForEachWithBinding(set: $set)
        
    }
}

struct SetforForEachWithBinding: View {
    
    @Binding var set: Set<String>
    
    var body: some View {
        
        List {
            ForEach(set.sorted(by: <), id: \.self) { element in
                
                CustomTextFieldView(string: Binding(
                    get: { return element },
                    set: { newValue in
                        
                        set.remove(element)
                        set.insert(newValue)
                        
                    }
                ))
                
            }
        }
    }
}





struct CustomTextFieldView: View {
    
    @Binding var string: String
    @State private var isUnderEdit: Bool = Bool()
    @State private var isUnderEditString: String = String()
    
    var body: some View {
        TextField("Enter value here...", text: Binding(
            get: {
                
                if isUnderEdit {
                    return isUnderEditString
                }
                else {
                    return string
                }
                
            },
            set: { newValue in
                
                if isUnderEdit {
                    isUnderEditString = newValue
                }
                
            }
        ), onEditingChanged: { value in
            
            if value {
                isUnderEdit = true
                isUnderEditString = string
            }
            else {
                isUnderEdit = false
                string = isUnderEditString
            }
            
        }, onCommit: { })
        
    }
}

Upvotes: 1

Related Questions