Using the compactMap Operator in Combine with SwiftUI

|

In Combine, the compactMap operator allows you to transform values while filtering out any nil results. This operator is particularly useful in situations where you’re working with optional data and want to ignore any nil values, passing only non-nil data through the pipeline.

What is the compactMap Operator?

The compactMap operator in Combine takes each value emitted by a publisher and applies a transformation that can return an optional. If the result of the transformation is nil, it’s discarded. Only non-nil values are allowed to continue through the data stream. This is useful when dealing with values that may or may not be present, such as optional data from a text input or network response.

For example, you might use compactMap to:

  • Filter out invalid data from user input.
  • Transform optional values and ignore nil results.
  • Filter and transform data based on specific criteria.

In this article, we’ll create a SwiftUI view that uses compactMap to process text input from a user, attempting to convert the input to an integer. Only valid integer values will be displayed.

Example SwiftUI View with compactMap

Below is a SwiftUI view that uses a PassthroughSubject to emit user input values from a text field. The compactMap operator filters out any input that cannot be converted to an integer, so only valid integers are displayed in the view.

import SwiftUI
import Combine

struct CompactMapExampleView: View {
    // State to hold the user's text input
    @State private var userInput: String = ""
    
    // State to display the last valid integer
    @State private var validNumberText: String = "Enter a number"
    
    // PassthroughSubject to publish user inputs
    private let inputPublisher = PassthroughSubject<String, Never>()
    
    // AnyCancellable to store the subscription
    @State private var cancellable: AnyCancellable?
    
    var body: some View {
        VStack(spacing: 20) {
            Text("CompactMap Operator Example")
                .font(.headline)
                .padding()
            
            TextField("Type a number", text: $userInput)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onChange(of: userInput) { oldState, newState in
                    inputPublisher.send(newState)
                }
            
            Text(validNumberText) // Display the last valid integer
                .font(.title2)
                .padding()
                .multilineTextAlignment(.center)
        }
        .onAppear {
            // Use compactMap to attempt to convert each input to an Int and discard non-numeric inputs
            cancellable = inputPublisher
                .compactMap { Int($0) } // Attempts to convert the input to an integer, ignoring nil results
                .sink { validNumber in
                    validNumberText = "Valid Number: \(validNumber)"
                }
        }
        .onDisappear {
            // Cancel the subscription when the view disappears
            cancellable?.cancel()
        }
    }
}

struct CompactMapExampleView_Previews: PreviewProvider {
    static var previews: some View {
        CompactMapExampleView()
    }
}

Explanation of the Code

  1. State Variables:
    • @State private var userInput: Holds the text input from the user, which is updated each time the text field changes.
    • @State private var validNumberText: Displays the last valid integer entered by the user. This variable updates whenever compactMap emits a valid integer.
  2. TextField for User Input:
    • The TextField lets the user type any text. Each time the text changes, the .onChange(of:) modifier triggers, sending the current text to inputPublisher.
  3. The compactMap Operator:
    • Inside .onAppear, we use compactMap on inputPublisher. Each time a new input is received, compactMap attempts to convert it to an integer using Int($0). If the conversion fails (e.g., if the input is not a valid number), compactMap returns nil, which is filtered out of the stream.
    • Only non-nil (valid integer) values are allowed through, and .sink then updates validNumberText with the last valid integer, refreshing the view.
  4. Displaying the Valid Integer:
    • Each time compactMap emits a valid integer, .sink updates validNumberText, which SwiftUI observes and displays in the view.
  5. Cleaning Up the Subscription:
    • The .onDisappear modifier cancels the subscription when the view disappears, preventing any potential memory leaks.

How compactMap Helps in SwiftUI

In this example, compactMap lets us filter and transform the user’s text input, ensuring that only valid integers are displayed. Any non-numeric input is ignored, making it easy to handle optional values and prevent invalid data from reaching the view. This approach is especially useful for real-time input validation or filtering optional data.

Summary

The compactMap operator is a powerful tool in Combine for working with optional data. By filtering out nil values and transforming data in a single step, compactMap makes it easy to work with optional inputs, such as user text fields or network responses, and ensures only valid, non-nil values reach your SwiftUI views. This helps create more responsive, user-friendly apps by focusing on meaningful data.

Leave a Reply

Your email address will not be published. Required fields are marked *