Extending the Guidebook App

Hi team,

Using the Guidebook application as a reference, I have ran into some issues trying to add a couple of specific enhancements, which I was hoping there might be some input on.

Firstly, I would like the user to be able to copy and paste content from any of the labels in the Detail View (the idea that they may wish to use some of this in their custom notes, or send text to other apps such as WhatsApp messages to friends, etc.).

If I just focus on a specific label, than this is pretty straightforward using the in-built GestureRecognizer and over-riding the copyMenu and copy functions. From viewDidLoad():

let tapGesture = UITapGestureRecognizer(target: self , action: #selector ((copyMenu( _ :))))

self.textBodyLabel.addGestureRecognizer(tapGesture)

Then, in the copy method, the contents of the label gets added to the pasteboard through “UIPasteboard.general.string = self.textBodyLabel.text”

The issue comes when trying to copy the text from a different label (for example, the address label).

I assumed I could do this pretty easily using tags and a Switch statement to copy the text of the label that was selected (double-clicked or long-pressed). However, I have been unable to get this to work. If I call self.view.addGestureRecognizer(tapGesture), the value of tag is always returned as 0 (the value for the view, rather than a specific element in it) so the Switch statement does not get executed properly. Similarly, the tag is always returned as 0 within viewDidLoad().

Essentially, how do we determine which label has been selected (double-clicked or long-pressed) so that action can be performed accordingly? I understand that standard approach would generally be to use buttons but that would not be applicable here.

I have explicitly set isUserInteractionEnabled to true for each label, both programatically and in Main.Storyboard, but that makes no difference (which is what I expected, since I believe this is true by default, and wouldn’t explain why it works when I focus on just one specific label).

The second roadblock I have encountered is regarding segues and view controller transitions.

I have expanded upon Chris’s example and separated the Favourites list into it’s own View Controller. This works fine but there is a behavioural issue with respect to the Back Button within the DetailViewController.

In Chris’s example, he uses “navigationController?.popViewController(animated: true)” when the Back Button is tapped. For me, this works fine for the initial traversing between PlacesVC and DetailVC. However, once the FavouritesViewController is loaded, all subsequent attempts at popping the ViewController fail (i.e. nothing happens when the Back Button is selected). A few points to note about this:

  • This happens regardless of whether the transition from PlacesVC to FavouritesVC is done via a Segue or by calling instantiateViewController.
  • I use the same kind of segue to get from FavouritesVC to DetailVC as I do from PlacesVC to DetailVC.
  • Calling popToRootViewController gives the same behaviour.

I then tried using the Back Button tapped method to call self.dismiss(animated: true, completion: nil), which yielded some interesting behaviour.

  • This fails to dismiss the very first time the DetailViewController is loaded after selecting a location from the PlacesVC.
  • However, if I then navigate back to either the PlacesVC or FavouritesVC directly (using buttons I added) and then load the DetailVC again by selecting a place from the menu, this works fine (as does all subsequents attempts to use the Back Button).
  • Additionally, if *the user initially goes from PlacesVC to FavesVC and then loads the DetailVC by selecting a location from the list of Favourites, the Back button works the first time.
  • Thus, it’s only when going from PlacesVC to DetailVC the first time that the Back button fails.
  • I experimented with changing from *a Segue to calling instantiateViewController(withIdentifier: Constants.Storyboard.detailViewController) within PlacesVC (at didSelectRowAt) but this caused a run-time crash whenever the user clicked on a location from the menu.

I suspect I may not be fully understanding how to best use Segues but have spent quite a bit of time on this without being able to figure it out completely, so any pointers would be greatly appreciated.

Finally, what is the best way to get URL’s that are in the text to appear as clickable links rather than plain text, when being read from the realm file?

I appreciate any assistance with any of these three items and I hope that I have properly articulated each one and that each scenario should be easily reproducible from Chris’s Guidebook application.

Thanks!

Mark

why did you use self.view.addGestureRecognizer(tapGesture) when you wanted to copy from a specific label like address label. it should be self.addresslabel.addGestureRecognizer(tapGesture) right?, similar to your self.textBodyLabel.addGestureRecognizer(tapGesture)

and for the viewcontrollers. do you have arrows pointing to the favorites viewcontroller in your storyboard? i think your favorites viewcontroller is a “standalone” page that’s why it doesn’t navigate correctly

Hi there,

Just a follow-up to my post from yesterday.

It looks like my issue with the Segues was that they all needed to be set in the Main.Storyboard to have a Kind value of ‘Show’, as opposed to ‘Show Detail’, which is what had been set by default.

Once, I set this for all the Segues, and used Segues for all of my transitions between View Controllers, navigation now seems to be working fine (fingers crossed).

I would still really appreciate any input on the two other requests I raised in yesterday’s post, which I remain stuck on. For clarity, I will repeat these below (as I know yesterday’s post was pretty long):

First Request:

I would like the user to be able to copy and paste content from any of the labels in the Detail View (the idea that they may wish to use some of this in their custom notes, or send text to other apps such as WhatsApp messages to friends, etc.).

If I just focus on a specific label, than this is pretty straightforward using the in-built GestureRecognizer and over-riding the copyMenu and copy functions. From viewDidLoad():

let tapGesture = UITapGestureRecognizer(target: self , action: #selector ((copyMenu( _ :))))

self.textBodyLabel.addGestureRecognizer(tapGesture)

Then, in the copy method, the contents of the label gets added to the pasteboard through “UIPasteboard.general.string = self.textBodyLabel.text”

The issue comes when trying to copy the text from a different label (for example, the address label).

I assumed I could do this pretty easily using tags and a Switch statement to copy the text of the label that was selected (double-clicked or long-pressed). However, I have been unable to get this to work. If I call self.view.addGestureRecognizer(tapGesture), the value of tag is always returned as 0 (the value for the view, rather than a specific element in it) so the Switch statement does not get executed properly. Similarly, the tag is always returned as 0 within viewDidLoad().

Essentially, I need to be able to determine which label has been selected (double-clicked or long-pressed) so that the appropriate action can be performed (in this case, copying the text of the label). I understand that standard approach would generally be to use buttons but that would not be applicable here.

I have explicitly set isUserInteractionEnabled to true for each label, both programatically and in Main.Storyboard, but that makes no difference (which is what I expected, since I believe this is true by default, and wouldn’t explain why it works when I focus on just one specific label).

Second Request:

Finally, what is the best way to get URL’s that are in the text to appear as clickable links rather than plain text, when being read from the realm file? Do I need to use the TTTAttributedLabel? I added this to my Podfile and installed and imported into my Swift code, but have had no success getting the links to appear when reading from the label text. Thus, if there is a working example of how this works in any of Chris’s tutorials, I would really appreciate it.

Thanks!

Mark

Hi Francis,

Thanks so much for taking the time to look into this!

Your response came in just as I was posting a follow-up reply. I think I am all set with the navigation aspect of yesterday’s post (see my post from a few minutes ago).

With respect to the addGestureRecognizer question(s), using self.addresslabel.addGestureRecognizer(tapGesture) does work fine if I know that there is only one specific label that the text is to be copied from (i.e. I’m hardcoding addresslabel).

If the user is to be allowed to copy the text from any of the labels, how can we capture the label that the user selected and assign that to addresslabel? I tried using tags, but wasn’t able to get this to work.

Cheers - Mark

oh ok so what you want is to copy the “label” along with the text that comes with it

maybe just capture it in the pasting part

so put a condition where the paste is coming from then append the text with it

maybe compare the UIPasteboard.general.string to self.[whatever label].text and see if it matches

then doing something like

UIPasteboard.general.string = self.textBodyLabel.text
should be
UIPasteboard.general.string = "Body: " + self.textBodyLabel.text

thus addresslabel pasting should be

UIPasteboard.general.string = "Address: " + self.textBodyLabel.text

its a manual workaround but it should work fine :slight_smile:

Thanks, but I already have that. I think I am probably over-complicating things in my description.

Before I get to the stage you mention, how do I determine what label has been selected (double-clicked or long-pressed) by the user?

^added this an an edit just as you replied

if its a match then its probably that label :joy:

Thanks, Francis. However, that seems to be more what one would use when pasting.

I need to determine what to copy into the pasteboard in the first place (i.e. how to identify the label whose text will be assigned to the pasteboard).

can you show your code for the recognizer where you handle your touch/tap?

it should look like
@IBAction func handle[gesture] (recognizer:UI[gesture]GestureRecognizer) {
}

In the viewDidLoad(), I have the following:

    //create an instance of UITapGestureRecognizer, calling copyMenu to configure where the 'Copy' pop-up appears
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector((copyMenu(_:))))        

    //specify that it requires two taps
    tapGesture.numberOfTapsRequired = 2


    self.view.addGestureRecognizer(tapGesture)

The copyMenu function is as follows:

//Configure the Copy option that will appear when the user performs a gesture that is recognized*
@objc func copyMenu(_ gestureRecognizer: UIGestureRecognizer) {

    //create an instance of the Menu Controller
    let menu = UIMenuController.shared

    if !menu.isMenuVisible {

        //200 chosen as width by trial and error as it seems to be a good place for the menu to appear
        //height of 0 seems to centre it nicely
        menu.setTargetRect(CGRect(x: self.textBodyLabel.center.x, y: self.textBodyLabel.center.y, width: 0.0, height: 200.0), in: view)

        menu.setMenuVisible(true, animated: true)
    }
}

Over-riding the copy function to copy the text to the pasteboard:

//copy current Text to the paste board
override func copy(_ sender: Any?) {

    switch (self.view.tag) {
    case 11:
        nameLabel.becomeFirstResponder()
        UIPasteboard.general.string = self.nameLabel.text
        break
    case 12:
        addressLabel.becomeFirstResponder()
        UIPasteboard.general.string = self.addressLabel.text
        break
    case 13:
        summaryLabel.becomeFirstResponder()
        UIPasteboard.general.string = self.summaryLabel.text
        break
    case 14:
        textBodyLabel.becomeFirstResponder()
        UIPasteboard.general.string = self.textBodyLabel.text
        break
    case 15:
        relatedSitesLabel.becomeFirstResponder()
        UIPasteboard.general.string = self.relatedSitesLabel.text
        break
    default:
        UIPasteboard.general.string = self.textBodyLabel.text
        break
    }        

}

Here, the Switch statement just falls through to the default every time. This leads me to believe that I am not using the tags correctly to get the label that was selected, so I think that’s primarily what I need help with.

Thanks for your continued assistance!

Mark

in your copyMenu function try to make use of the gestureRecognizer variable

according to the documentation, UIGestureRecognizer has the property “view” with contains the view the gesture came from (https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624212-view)

thus,

if you make a global UIView variable , lets say tempView
you can save where the gesture is from temporarily

tempView = gestureRecognizer.view

then based on that you can use tempView to get the needed text
self.tempView.text

you can also combo it with my workaround before so you can check it on which label it belongs
so
if self.tempview.text == self.nameLabel.text
etc

Thanks, Francis.

While this does look like it may be the way to go, self.tempView.text yields the error within the copyMenu function:

Value of type ‘UIView’ has no member ‘text’; did you mean ‘next’?

There is no ‘text’ option from the ‘view.’ dropdown (hence the error), while none of the options I tested (’.description’, ‘.gestureRecognizers’, ‘.snapshotView’ return the actual text of what was contained in the label.

var tempView:UIView = UIView()


let tapGesture = UITapGestureRecognizer(target: self , action: #selector ((copyMenu( _ :))))


@objc func copyMenu(_ gestureRecognizer: UIGestureRecognizer) {
tempView = gestureRecognizer.view!
print(self.tempView.text) //Attempting to do anything with self.tempView.text yields the error

Output of printing both gestureRecognizer.view and gestureRecognizer.view.description to the console:

temp view: <UIView: 0x7fd53967c330; frame = (0 0; 414 896); autoresize = W+H; gestureRecognizers = <NSArray: 0x600000dd1200>; layer = <CALayer: 0x60000027a160>>

can you check if it has a .subview? if not it should have the coordinates (0 0; 414 896) of the specified UIView so technically you have an idea where the gesture is coming from

would be very tricky though

When setting tempView = gestureRecognizer.view!.subviews (after changing the tempView declaration to be var tempView:[UIView] = UIView), it provides all of the elements within the view, below, but thus far doesn’t help me determine which of these subviews (i.e. labels) was selected.

tempView: [<UILabel: 0x7fbbd0f0b0b0; frame = (0 44; 414 20.5); text = ‘SOHO STREETS: Bag O’Nails’; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000010d7b10>>, <UIImageView: 0x7fbbd0f0a9a0; frame = (0 74.5; 414 200); clipsToBounds = YES; opaque = NO; autoresize = RM+BM; tag = 10; layer = <CALayer: 0x6000030e4760>>, <UIStackView: 0x7fbbd0f0bd50; frame = (10 74.5; 40 200); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x6000030e4e00>>, <UIStackView: 0x7fbbd0f0c160; frame = (0 284.5; 414 30); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x6000030e6b40>>, <UIStackView: 0x7fbbd0f0c360; frame = (0 324.5; 414 30); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x6000030e6ea0>>, <UIScrollView: 0x7fbbd500ba00; frame = (0 364.5; 414 531.5); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x600003e968b0>; layer = <CALayer: 0x6000030e7200>; contentOffset: {0, 0}; contentSize: {414, 552}; adjustedContentInset: {0, 0, 34, 0}>]

it literally says here that this is the text, so technically the .subview is what you are looking for

there is even the tag that you where setting up

you can get all these values just by doing gestureRecognizer.view.subview.text (tempview.subview.text) and gestureRecognizer.view.subview.tag (tempview.subview.tag)

Thanks, Francis, but I am afraid you are missing the point here.

I can obviously see that text in the output, but that is not the label I selected.

The output I showed in my previous post is clearly for the whole view. The reason that only one text string appears in this output is that the other labels are nested within stack views and a scroll view (and presumably subviews just returns results one level down).

Also, if you review this output again, you will see that the tag of ‘10’ is for a UIImageView and does not correspond to the label that has the text you are quoting (I even sanity checked this in my Main.Storyboard).

If I reformat the output, this should make it clearer for you:

  • <UILabel: 0x7fbbd0f0b0b0; frame = (0 44; 414 20.5); text = ‘SOHO STREETS: Bag O’Nails’; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000010d7b10>>,

  • <UIImageView: 0x7fbbd0f0a9a0; frame = (0 74.5; 414 200); clipsToBounds = YES; opaque = NO; autoresize = RM+BM; tag = 10; layer = <CALayer: 0x6000030e4760>>,

  • <UIStackView: 0x7fbbd0f0bd50; frame = (10 74.5; 40 200); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x6000030e4e00>>,

  • <UIStackView: 0x7fbbd0f0c160; frame = (0 284.5; 414 30); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x6000030e6b40>>,

  • <UIStackView: 0x7fbbd0f0c360; frame = (0 324.5; 414 30); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x6000030e6ea0>>,

  • <UIScrollView: 0x7fbbd500ba00; frame = (0 364.5; 414 531.5); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x600003e968b0>; layer = <CALayer: 0x6000030e7200>; contentOffset: {0, 0}; contentSize: {414, 552}; adjustedContentInset: {0, 0, 34, 0}>

]

Perhaps you might want to try this out yourself?

I have to imagine I am not the only one who has tried this functionality and I would hope that it should be pretty easy to add this to the Guidebook app.

Thanks - Mark

I think I have this figured out. I will update the thread with details once I have tested more.

For the record, I used the location method to get the coordinates of where the gesture takes place:

//Define a variable for the coordinates tapped on by the user
var tappedCoordinates:CGPoint?

tappedCoordinates = gestureRecognizer.location(in: View)

I then had to track the position of each label in the view along the y axis and compare the tappedCoordinates!.y value to each.

The ‘UIPasteboard.general.string’ was then set based on the results (i.e. which label the tappedCoordinates!.y value fell in).

One thing that needed to be taken into account was when the contents of the view were longer than the standard view and scrolling was required. Luckily, scrollView.contentOffset.y was useful here and this simply needed to be added to the tappedCoordinates!.y to get an updated view:

tappedCoordinates!.y += scrollView.contentOffset.y

It’s far from elegant but appears to be working thus far.

I’d still really like to know if there are any tutorials where URL text is displayed as a clickable (and, if not, would appreciate one being created, but perhaps I should start a new thread for that.