Last post, we talked about the TagView but it was only view only. For this post, we talk about leveraging EditMode to allow TagView for both viewing and editing the data.

All of the code for this blog post is in this sample code repo.

Switching to State Objects

To shift from a static view to a view that can be changed, we need to make some minor changes in our view.

Step one:

Add an EditMode variable, so that we can detect whether we’re in an edit state or not.

    @State private var editMode: EditMode = .inactive

Then we shift from a constant value to a @State values that can be changed.

// Removed: let tag: Tag
    @State var tag: Tag
    @State var tagPlaceHolder: String

This is needed because we’re going to be using a TextField which allows you to change String values. As the tagPlaceHolder will always be tied to the value of the tag itself, let’s make life easier for ourselves by adding an initializer.

    init(editMode: EditMode = .inactive,
         tag: Tag) {
        self.editMode = editMode
        self.tag = tag
        self.tagPlaceHolder = tag.toString
    }

This way we can continue to use the same initializer that we had been using. ie; TagView(tag: Tag("tag")) without adding a chance that the tag value and the tagPlaceHolder could ever get out of sync.

Step two:

Now we just have to change the body property to leverage the editMode state and either show a static view or TextField that will allow a user to change the value.

First, we need to add a way to toggle the editMode. I added a long press gesture to the static view to toggle the value.

	.onLongPressGesture {
		editMode = .active
	}

Step three:

Now that we can have different editMode states, we can leverage them with an if/else block.

	if editMode == .active {
			TextField(Constants.Tag.placeholder,
					  text: $tagPlaceHolder,
					  axis: .vertical)
			.textFieldStyle(.plain)
			.multilineTextAlignment(.center)
			.onSubmit {
				editMode = .inactive
				convertToTag(placeholder: tagPlaceHolder,
							 baseTag: tag)
			}
	} else {
		Text("@\(tag.tag)")
		if let payload = tag.payload {
			Text("@\(tag.tag)(**\(payload)**)")
		} else {
			Text("@\(tag.tag)")
		}
	}

Using the .onSubmit for the TextField allows us to toggle the editMode to reset the view and our new convertToTag function to manage the logic for the conversion from a String to a Tag.

While the Text to TextField are visually different; I felt that the overlay should also change to better illustrate the change.

	Group {
		if editMode == .active {
			RoundedRectangle(cornerRadius: 8)
				.stroke(style: StrokeStyle(lineWidth: 1,
										   dash: [2]))
				.foregroundColor(Color.Tag.border)
		} else {
			Capsule()
				.stroke(Color.Tag.border,
						lineWidth: 1)
		}
	}

Personally, I felt that the dashed rounded rectangle looked a little better as the TextField expands.

Step four:

Lastly, we fill in the placeholder for the convertToTag logic.

    func convertToTag(placeholder: String,
                      baseTag: Tag) {
        guard let convertedTag = placeholder.toTag() else {
            tagPlaceHolder = baseTag.toString
            return
        }

        self.tag = convertedTag
        self.tagPlaceHolder = convertedTag.toString
    }

This makes sure that the text converts to a Tag. Otherwise, we reset our tagPlaceholder back to the original this way incorrect strings don’t linger for the next time that the TextField is shown. Feel free to comment that bit out and see what you get.

If it’s properly converted, we reset the tag (and the tagPlaceHolder, so that the edge cases for a tag without a placeholder don’t result in a tag of @tag and a saved tagPlaceholder of @tag()).

Unfortunately, our existing extract tags conversion code is for Substring objects not String objects, so we needed a new extension to put in that logic. This also allowed us to put in some business logic to ensure that the tag conversion works when this is exactly one tag in the entered text.

    func toTag() -> Tag? {
        guard self.contains("@") else { return nil }

        let tags = Substring(self).extractTags()

        // We only want to convert from `String` to `Tag`, when there's one and only one tag in it.
        guard tags.count == 1 else { return nil }

        return tags.first
    }

Seeing it in action

We walk through the following cases:

  • Text that isn’t a tag.
  • Text that is multiple tags.
  • Text with a tag without a payload.
  • Text with a tag with a payload.
  • Text with a very large payload to verify wrapping.

TagView being edited


Next post, we will pull back a bit to talk about the different architectural options such as MVVM, VIPER, or TCA for a SwiftUI project, before we get too deep into how we’ve defined our UI.