1

Given a TupleView and ForEach containing Text(...).tag(...):

// TupleView
Text("Zero").tag(0)
Text("One").tag(1)
Text("Two").tag(2)

// ForEach
let values: [String] = ["Zero", "One", "Two"]
ForEach(values.indices, id: \.self) { index in
    Text(values[index]).tag(index)
}

How do I reflect back the .tag(...) and inner Text(String) value?

I want to build a custom Picker-like View. Apple's Picker is able to find the matching Text based on selection and preview the selected value.

0

1 Answer 1

1

Using Mirror and extensions on ForEach and TupleView:

import SwiftUI

func getTaggedText<Tag: Hashable, Content: View>(@ViewBuilder content: () -> Content) -> [(Tag, String)] {
    guard let gettable = content() as? GetTaggedText else {
        return []
    }
    return gettable.getTaggedText()
}

print(getTaggedText {
    Text("Zero").tag(0)
    Text("One").tag(1)
    Text("Two").tag(2)
} as [(Int, String)])

let values: [String] = ["Zero", "One", "Two"]
print(getTaggedText {
    ForEach(values.indices, id: \.self) { index in
        Text(values[index]).tag(index)
    }
} as [(Int, String)])

//
// All the reflection code
//

protocol GetTaggedText {
    func getTaggedText<Tag: Hashable>() ->  [(Tag, String)]
}

// view: The modified Text(...).tag(...) view
func extractTaggedText<Tag: Hashable>(_ view: Any) -> (Tag, String)? {
    let mirror = Mirror(reflecting: view)
    // Get the tag trait
    guard let tag = mirror.descendant("modifier", "value", "tagged") as? Tag else {
        return nil
    }
    // Unwrap Text(...) from tag trait
    guard let text = mirror.descendant("content") as? Text else {
        return nil
    }
    let textMirror = Mirror(reflecting: text)
    // get String from Storage -> String
    if let string = textMirror.descendant("storage", "verbatim") as? String {
        return (tag, string)
    }
    // get String from Stroage -> LocalizedStringKey
    if let string = textMirror.descendant("storage", "anyTextStorage", "key", "key") as? String {
        return (tag, string)
    }
    return (tag, "")
}

extension TupleView: GetTaggedText {
    func getTaggedText<Tag: Hashable>() ->  [(Tag, String)] {
        guard let tuple = Mirror(reflecting: self).descendant("value") else {
            return []
        }
        let values = Mirror(reflecting: tuple)
        let count = values.children.count
        var result: [(Tag, String)] = []
        for index in 0..<count {
            // Get individual View within tuple
            guard let view = values.descendant(".\(index)") else {
                break
            }
            guard let taggedText: (Tag, String) = extractTaggedText(view) else {
                continue
            }
            result.append(taggedText)
        }
        return result
    }
}

extension ForEach: GetTaggedText where Content: View {
    func getTaggedText<Tag: Hashable>() ->  [(Tag, String)] {
        var result: [(Tag, String)] = []
        for v in data {
            let view = content(v)
            guard let taggedText: (Tag, String) = extractTaggedText(view) else {
                continue
            }
            result.append(taggedText)
        }
        return result
    }
}

Results of prints:

[(0, "Zero"), (1, "One"), (2, "Two")]
[(0, "Zero"), (1, "One"), (2, "Two")]

(Sources that helped me figure this out: this post and this gist).

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.