Supporting Writing Tools on your app

Supporting Writing Tools on your app

Learn how to have access and manage support to Writing Tools within text fields.

Writing Tools is one of the main features included in Apple operational systems through Apple Intelligence, allowing users to easily rephrase text using local specialized machine learning models. Let's focus on how developers can take advantage of it in their apps.

Before diving into how we can customize the usage of Writing Tools within our app, it’s important to understand that they are a system feature that is automatically compatible with UIKit and SwiftUI components. Developers using the native text components of these frameworks don’t need to make any extra effort to support this feature in their apps.

On the other hand, there are limited customization options when it comes to implementing a custom version of Writing Tools. Despite this limitation, there are still a few tweaks we can make to better integrate them into our app functionalities.

Defining the behaviors

One way to customize Writing Tools is by defining how they are presented to the user and the type of assistance they provide, based on the app’s specific needs. You can do that by specifying the writingToolsBehavior property of a text view.

When this property is not explicitly set, the system automatically determines the most suitable experience based on the state of the text view. For example, if a UITextView is not editable, Writing Tools by default will appear as a pop-up or a modal depending on the platform. By explicitly setting this property, developers adjust the presentation and behavior to align with their app’s requirements.

The Writing Tools behavior property can be set with a value of type UIWritingToolsBehavior. At the moment there are three options available:

  • complete: In this mode, the text is directly replaced with the elaboration provided by the Writing Tools with a fancy animation. Of course, the user has the possibility to confirm or to revert the change.
  • limited: Here, the manipulated text is presented within a pop-up or modal dialog, offering options to copy the text. If the UITextView is editable, users can choose to replace the original text with the one produced by the Writing Tools.
  • none: with this property, the text view won't provide any support for Writing Tools.
import UIKit

struct CustomTextView: UIViewRepresentable {
    @Binding var text: String
    
    let onStartTimer: () -> Void
    let onStopTimer: () -> Void

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.text = text
        textView.isEditable = true
        textView.font = UIFont.systemFont(ofSize: 17, weight: .regular)
        
        // Define writing tools behavior for the view
        textView.writingToolsBehavior = .complete

        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
        if textView.text != text {
            textView.text = text
        }
    }
}

Tracking the Writing Tools usage

Once we have decided how the Writing Tools will power the UITextView we can programmatically track their usage and take action if and when needed. We can easily achieve that by defining a Coordinator class and adhering to the delegation pattern to have deeper control over the Writing tools elaboration stage.

import UIKit

struct CustomTextView: UIViewRepresentable {
    @Binding var text: String
    
    let onStartTimer: () -> Void
    let onStopTimer: () -> Void

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.text = text
        textView.isEditable = true
        textView.font = UIFont.systemFont(ofSize: 17, weight: .regular)
        
        textView.writingToolsBehavior = .complete

        // 1. Set up the coordinator object as the text view delegate
        textView.delegate = context.coordinator

        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
        if textView.text != text {
            textView.text = text
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    // Declaration of the coordinator of the text view conforming with the UITextViewDelegate protocol
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: CustomTextView

        init(parent: CustomTextView) {
            self.parent = parent
        }
    }
}

In the example above there are two important steps that were taken:

  1. Defining the coordinator object as the delegate of the text view
    Within the makeUIView(context:) method, we assign this class as the delegate for the UITextView.
  2. Connecting the custom text view to the coordinator
    The declared Coordinator class has a property called parent which represents the CustomTextView object.

Writing Tools started

To monitor when Writing Tools is summoned by the user implement the textViewWritingToolsWillBegin(_:) method, defined in the UITextViewDelegate protocol, in the Coordinator class.

import UIKit

struct CustomTextView: UIViewRepresentable {
    @Binding var text: String
    
    let onStartTimer: () -> Void
    let onStopTimer: () -> Void

    func makeUIView(context: Context) -> UITextView { ... }

    func updateUIView(_ textView: UITextView, context: Context) { ... }

    func makeCoordinator() -> Coordinator { ... }

    class Coordinator: NSObject, UITextViewDelegate {
        var parent: CustomTextView

        init(parent: CustomTextView) {
            self.parent = parent
        }

        func textViewWritingToolsWillBegin(_ textView: UITextView) {
            // Start the timer when Writing Tools is summoned
            parent.onStartTimer() 
        }
    }
}

In the example above as soon as the textViewWritingToolsWillBegin(_:) is called the timer starts performing the onStartTimer() function passed as a parameter to our custom view.

This method could be used when you need to pass text to some asynchronous function connected to network calls or when you synchronize the textual information with cloud services, for example.

Writing Tools finished

To monitor when Writing Tools is closed by the user implement the textViewWritingToolsDidEnd() method, defined in the UITextViewDelegate protocol, in the Coordinator class.

import UIKit

struct CustomTextView: UIViewRepresentable {
    @Binding var text: String
    let onStartTimer: () -> Void
    let onStopTimer: () -> Void

    func makeUIView(context: Context) -> UITextView { ... }

    func updateUIView(_ textView: UITextView, context: Context) { ... }

    func makeCoordinator() -> Coordinator { ... }

    class Coordinator: NSObject, UITextViewDelegate {
        var parent: CustomTextView

        init(parent: CustomTextView) {
            self.parent = parent
        }

        func textViewWritingToolsWillBegin(_ textView: UITextView) { ... }

        func textViewWritingToolsDidEnd(_ textView: UITextView) {
            // Stop the timer when Writing Tools is dismissed
            parent.onStopTimer()
        }
        
        func textViewDidChange(_ textView: UITextView) {
            if parent.text != textView.text {
                parent.text = textView.text
            }
        }
    }
}

In this way, an action can be performed when the Writing Tools finishes to manipulate the text inside the text view. In this example, we call the function that stops the timer, but you could use this method to restore network requests or cloud synchronization actions.

Implementation in SwiftUI

Now that we have defined our custom text view component it's time to display it in a SwiftUI View.

struct ContentView: View {

    @State var textContent: String = """
      On a quiet autumn evening, the golden leaves danced in the wind, whispering stories of summer's end.
      The sun dipped below the horizon, painting the sky in shades of pink and orange. Nature sighed in contentment, preparing for a peaceful slumber.
      """
    
    @State var elapsedTime: TimeInterval = 0
    @State var timer: Timer? = nil

    var body: some View {
        NavigationStack {
            Form {
                CustomTextView(
                    text: $textContent,
                    onStartTimer: startTimer,
                    onStopTimer: stopTimer
                )
                .frame(height: 300)

                HStack {
                    Spacer()
                    Text("Elapsed Time: \(String(format: "%.2f", elapsedTime)) seconds")
                        .font(.headline)
                        .foregroundColor(.gray)
                    Spacer()
                }
            }
            .navigationTitle("Writing Tools ✏️")
        }
    }

    func startTimer() {
        stopTimer() // Stop any existing timer before starting a new one
        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in
            elapsedTime += 0.01
        }
    }

    func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
}
Writing Tools at the moment can't be tested on the simulator. Use a device with Apple Intelligence enabled to have access to it when testing your application.

Developers need to rely on text components from UIKit such as UITextView and UITextField to have the maximum control over how Writing Tools operates with the content of their apps. Despite few customization options, developers can take the experience of using writing tools to another level, enhancing the way user interact with text.